feat(tools): add ast-grep tools for AST-aware code search and transformation
- Add ast_grep_search for pattern-based code search (25 languages) - Add ast_grep_replace for AST-aware code rewriting with dry-run - Add ast_grep_analyze for in-memory code analysis (NAPI) - Add ast_grep_transform for in-memory code transformation - Add ast_grep_languages to list supported languages
This commit is contained in:
21
bun.lock
21
bun.lock
@@ -5,6 +5,7 @@
|
||||
"": {
|
||||
"name": "oh-my-opencode",
|
||||
"dependencies": {
|
||||
"@ast-grep/napi": "^0.40.0",
|
||||
"@opencode-ai/plugin": "^1.0.7",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -17,6 +18,26 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
|
||||
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
|
||||
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.128", "", { "dependencies": { "@opencode-ai/sdk": "1.0.128", "zod": "4.1.8" } }, "sha512-M5vjz3I6KeoBSNduWmT5iHXRtTLCqICM5ocs+WrB3uxVorslcO3HVwcLzrERh/ntpxJ/1xhnHQaeG6Mg+P744A=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.128", "", {}, "sha512-Kow3Ivg8bR8dNRp8C0LwF9e8+woIrwFgw3ZALycwCfqS/UujDkJiBeYHdr1l/07GSHP9sZPmvJ6POuvfZ923EA=="],
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/code-yeongyu/oh-my-opencode#readme",
|
||||
"dependencies": {
|
||||
"@ast-grep/napi": "^0.40.0",
|
||||
"@opencode-ai/plugin": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
66
src/tools/ast-grep/cli.ts
Normal file
66
src/tools/ast-grep/cli.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { spawn } from "bun"
|
||||
import { SG_CLI_PATH } from "./constants"
|
||||
import type { CliMatch, CliLanguage } from "./types"
|
||||
|
||||
export interface RunOptions {
|
||||
pattern: string
|
||||
lang: CliLanguage
|
||||
paths?: string[]
|
||||
globs?: string[]
|
||||
rewrite?: string
|
||||
context?: number
|
||||
updateAll?: boolean
|
||||
}
|
||||
|
||||
export async function runSg(options: RunOptions): Promise<CliMatch[]> {
|
||||
const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"]
|
||||
|
||||
if (options.rewrite) {
|
||||
args.push("-r", options.rewrite)
|
||||
if (options.updateAll) {
|
||||
args.push("--update-all")
|
||||
}
|
||||
}
|
||||
|
||||
if (options.context && options.context > 0) {
|
||||
args.push("-C", String(options.context))
|
||||
}
|
||||
|
||||
if (options.globs) {
|
||||
for (const glob of options.globs) {
|
||||
args.push("--globs", glob)
|
||||
}
|
||||
}
|
||||
|
||||
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."]
|
||||
args.push(...paths)
|
||||
|
||||
const proc = spawn([SG_CLI_PATH, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
|
||||
if (exitCode !== 0 && stdout.trim() === "") {
|
||||
if (stderr.includes("No files found")) {
|
||||
return []
|
||||
}
|
||||
if (stderr.trim()) {
|
||||
throw new Error(stderr.trim())
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(stdout) as CliMatch[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
63
src/tools/ast-grep/constants.ts
Normal file
63
src/tools/ast-grep/constants.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// ast-grep CLI path (homebrew installed)
|
||||
export const SG_CLI_PATH = "sg"
|
||||
|
||||
// CLI supported languages (25 total)
|
||||
export const CLI_LANGUAGES = [
|
||||
"bash",
|
||||
"c",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"css",
|
||||
"elixir",
|
||||
"go",
|
||||
"haskell",
|
||||
"html",
|
||||
"java",
|
||||
"javascript",
|
||||
"json",
|
||||
"kotlin",
|
||||
"lua",
|
||||
"nix",
|
||||
"php",
|
||||
"python",
|
||||
"ruby",
|
||||
"rust",
|
||||
"scala",
|
||||
"solidity",
|
||||
"swift",
|
||||
"typescript",
|
||||
"tsx",
|
||||
"yaml",
|
||||
] as const
|
||||
|
||||
// NAPI supported languages (5 total - native bindings)
|
||||
export const NAPI_LANGUAGES = ["html", "javascript", "tsx", "css", "typescript"] as const
|
||||
|
||||
// Language to file extensions mapping
|
||||
export const LANG_EXTENSIONS: Record<string, string[]> = {
|
||||
bash: [".bash", ".sh", ".zsh", ".bats"],
|
||||
c: [".c", ".h"],
|
||||
cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h"],
|
||||
csharp: [".cs"],
|
||||
css: [".css"],
|
||||
elixir: [".ex", ".exs"],
|
||||
go: [".go"],
|
||||
haskell: [".hs", ".lhs"],
|
||||
html: [".html", ".htm"],
|
||||
java: [".java"],
|
||||
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
||||
json: [".json"],
|
||||
kotlin: [".kt", ".kts"],
|
||||
lua: [".lua"],
|
||||
nix: [".nix"],
|
||||
php: [".php"],
|
||||
python: [".py", ".pyi"],
|
||||
ruby: [".rb", ".rake"],
|
||||
rust: [".rs"],
|
||||
scala: [".scala", ".sc"],
|
||||
solidity: [".sol"],
|
||||
swift: [".swift"],
|
||||
typescript: [".ts", ".cts", ".mts"],
|
||||
tsx: [".tsx"],
|
||||
yaml: [".yml", ".yaml"],
|
||||
}
|
||||
23
src/tools/ast-grep/index.ts
Normal file
23
src/tools/ast-grep/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
ast_grep_search,
|
||||
ast_grep_replace,
|
||||
ast_grep_languages,
|
||||
ast_grep_analyze,
|
||||
ast_grep_transform,
|
||||
} from "./tools"
|
||||
|
||||
export const builtinTools = {
|
||||
ast_grep_search,
|
||||
ast_grep_replace,
|
||||
ast_grep_languages,
|
||||
ast_grep_analyze,
|
||||
ast_grep_transform,
|
||||
}
|
||||
|
||||
export {
|
||||
ast_grep_search,
|
||||
ast_grep_replace,
|
||||
ast_grep_languages,
|
||||
ast_grep_analyze,
|
||||
ast_grep_transform,
|
||||
}
|
||||
106
src/tools/ast-grep/napi.ts
Normal file
106
src/tools/ast-grep/napi.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { parse, Lang } from "@ast-grep/napi"
|
||||
import type { NapiLanguage, AnalyzeResult, MetaVariable, Range } from "./types"
|
||||
|
||||
const LANG_MAP: Record<NapiLanguage, Lang> = {
|
||||
html: Lang.Html,
|
||||
javascript: Lang.JavaScript,
|
||||
tsx: Lang.Tsx,
|
||||
css: Lang.Css,
|
||||
typescript: Lang.TypeScript,
|
||||
}
|
||||
|
||||
export function parseCode(code: string, lang: NapiLanguage) {
|
||||
return parse(LANG_MAP[lang], code)
|
||||
}
|
||||
|
||||
export function findPattern(root: ReturnType<typeof parseCode>, pattern: string) {
|
||||
return root.root().findAll(pattern)
|
||||
}
|
||||
|
||||
function nodeToRange(node: ReturnType<ReturnType<typeof parseCode>["root"]>): Range {
|
||||
const range = node.range()
|
||||
return {
|
||||
start: { line: range.start.line, column: range.start.column },
|
||||
end: { line: range.end.line, column: range.end.column },
|
||||
}
|
||||
}
|
||||
|
||||
function extractMetaVariablesFromPattern(pattern: string): string[] {
|
||||
const matches = pattern.match(/\$[A-Z_][A-Z0-9_]*/g) || []
|
||||
return [...new Set(matches.map((m) => m.slice(1)))]
|
||||
}
|
||||
|
||||
export function extractMetaVariables(
|
||||
node: ReturnType<ReturnType<typeof parseCode>["root"]>,
|
||||
pattern: string
|
||||
): MetaVariable[] {
|
||||
const varNames = extractMetaVariablesFromPattern(pattern)
|
||||
const result: MetaVariable[] = []
|
||||
|
||||
for (const name of varNames) {
|
||||
const match = node.getMatch(name)
|
||||
if (match) {
|
||||
result.push({
|
||||
name,
|
||||
text: match.text(),
|
||||
kind: String(match.kind()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function analyzeCode(
|
||||
code: string,
|
||||
lang: NapiLanguage,
|
||||
pattern: string,
|
||||
shouldExtractMetaVars: boolean
|
||||
): AnalyzeResult[] {
|
||||
const root = parseCode(code, lang)
|
||||
const matches = findPattern(root, pattern)
|
||||
|
||||
return matches.map((node) => ({
|
||||
text: node.text(),
|
||||
range: nodeToRange(node),
|
||||
kind: String(node.kind()),
|
||||
metaVariables: shouldExtractMetaVars ? extractMetaVariables(node, pattern) : [],
|
||||
}))
|
||||
}
|
||||
|
||||
export function transformCode(
|
||||
code: string,
|
||||
lang: NapiLanguage,
|
||||
pattern: string,
|
||||
rewrite: string
|
||||
): { transformed: string; editCount: number } {
|
||||
const root = parseCode(code, lang)
|
||||
const matches = findPattern(root, pattern)
|
||||
|
||||
if (matches.length === 0) {
|
||||
return { transformed: code, editCount: 0 }
|
||||
}
|
||||
|
||||
const edits = matches.map((node) => {
|
||||
const metaVars = extractMetaVariables(node, pattern)
|
||||
let replacement = rewrite
|
||||
|
||||
for (const mv of metaVars) {
|
||||
replacement = replacement.replace(new RegExp(`\\$${mv.name}`, "g"), mv.text)
|
||||
}
|
||||
|
||||
return node.replace(replacement)
|
||||
})
|
||||
|
||||
const transformed = root.root().commitEdits(edits)
|
||||
return { transformed, editCount: edits.length }
|
||||
}
|
||||
|
||||
export function getRootInfo(code: string, lang: NapiLanguage): { kind: string; childCount: number } {
|
||||
const root = parseCode(code, lang)
|
||||
const rootNode = root.root()
|
||||
return {
|
||||
kind: String(rootNode.kind()),
|
||||
childCount: rootNode.children().length,
|
||||
}
|
||||
}
|
||||
158
src/tools/ast-grep/tools.ts
Normal file
158
src/tools/ast-grep/tools.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { tool } from "@opencode-ai/plugin/tool"
|
||||
import { CLI_LANGUAGES, NAPI_LANGUAGES, LANG_EXTENSIONS } from "./constants"
|
||||
import { runSg } from "./cli"
|
||||
import { analyzeCode, transformCode, getRootInfo } from "./napi"
|
||||
import { formatSearchResult, formatReplaceResult, formatAnalyzeResult, formatTransformResult } from "./utils"
|
||||
import type { CliLanguage, NapiLanguage } from "./types"
|
||||
|
||||
function showOutputToUser(context: unknown, output: string): void {
|
||||
const ctx = context as { metadata?: (input: { metadata: { output: string } }) => void }
|
||||
ctx.metadata?.({ metadata: { output } })
|
||||
}
|
||||
|
||||
export const ast_grep_search = tool({
|
||||
description:
|
||||
"Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " +
|
||||
"Use meta-variables: $VAR (single node), $$$ (multiple nodes). " +
|
||||
"Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
|
||||
args: {
|
||||
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$)"),
|
||||
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
||||
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
|
||||
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
|
||||
context: tool.schema.number().optional().describe("Context lines around match"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const matches = await runSg({
|
||||
pattern: args.pattern,
|
||||
lang: args.lang as CliLanguage,
|
||||
paths: args.paths,
|
||||
globs: args.globs,
|
||||
context: args.context,
|
||||
})
|
||||
const output = formatSearchResult(matches)
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const ast_grep_replace = tool({
|
||||
description:
|
||||
"Replace code patterns across filesystem with AST-aware rewriting. " +
|
||||
"Dry-run by default. Use meta-variables in rewrite to preserve matched content. " +
|
||||
"Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
|
||||
args: {
|
||||
pattern: tool.schema.string().describe("AST pattern to match"),
|
||||
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
||||
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
|
||||
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search"),
|
||||
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs"),
|
||||
dryRun: tool.schema.boolean().optional().describe("Preview changes without applying (default: true)"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const matches = await runSg({
|
||||
pattern: args.pattern,
|
||||
rewrite: args.rewrite,
|
||||
lang: args.lang as CliLanguage,
|
||||
paths: args.paths,
|
||||
globs: args.globs,
|
||||
updateAll: args.dryRun === false,
|
||||
})
|
||||
const output = formatReplaceResult(matches, args.dryRun !== false)
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const ast_grep_languages = tool({
|
||||
description:
|
||||
"List all supported languages for ast-grep tools with their file extensions. " +
|
||||
"Use this to determine valid language options.",
|
||||
args: {},
|
||||
execute: async (_args, context) => {
|
||||
const lines: string[] = [`Supported Languages (${CLI_LANGUAGES.length}):`]
|
||||
for (const lang of CLI_LANGUAGES) {
|
||||
const exts = LANG_EXTENSIONS[lang]?.join(", ") || ""
|
||||
lines.push(` ${lang}: ${exts}`)
|
||||
}
|
||||
lines.push("")
|
||||
lines.push(`NAPI (in-memory) languages: ${NAPI_LANGUAGES.join(", ")}`)
|
||||
const output = lines.join("\n")
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
},
|
||||
})
|
||||
|
||||
export const ast_grep_analyze = tool({
|
||||
description:
|
||||
"Parse code and extract AST structure with pattern matching (in-memory). " +
|
||||
"Extracts meta-variable bindings. Only for: html, javascript, tsx, css, typescript. " +
|
||||
"Use for detailed code analysis without file I/O.",
|
||||
args: {
|
||||
code: tool.schema.string().describe("Source code to analyze"),
|
||||
lang: tool.schema.enum(NAPI_LANGUAGES).describe("Language (html, javascript, tsx, css, typescript)"),
|
||||
pattern: tool.schema.string().optional().describe("Pattern to find (omit for root structure)"),
|
||||
extractMetaVars: tool.schema.boolean().optional().describe("Extract meta-variable bindings (default: true)"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
if (!args.pattern) {
|
||||
const info = getRootInfo(args.code, args.lang as NapiLanguage)
|
||||
const output = `Root kind: ${info.kind}\nChildren: ${info.childCount}`
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
}
|
||||
|
||||
const results = analyzeCode(args.code, args.lang as NapiLanguage, args.pattern, args.extractMetaVars !== false)
|
||||
const output = formatAnalyzeResult(results, args.extractMetaVars !== false)
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const ast_grep_transform = tool({
|
||||
description:
|
||||
"Transform code in-memory using AST-aware rewriting. " +
|
||||
"Only for: html, javascript, tsx, css, typescript. " +
|
||||
"Returns transformed code without writing to filesystem.",
|
||||
args: {
|
||||
code: tool.schema.string().describe("Source code to transform"),
|
||||
lang: tool.schema.enum(NAPI_LANGUAGES).describe("Language"),
|
||||
pattern: tool.schema.string().describe("Pattern to match"),
|
||||
rewrite: tool.schema.string().describe("Replacement (can use $VAR from pattern)"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const { transformed, editCount } = transformCode(
|
||||
args.code,
|
||||
args.lang as NapiLanguage,
|
||||
args.pattern,
|
||||
args.rewrite
|
||||
)
|
||||
const output = formatTransformResult(args.code, transformed, editCount)
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
showOutputToUser(context, output)
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
53
src/tools/ast-grep/types.ts
Normal file
53
src/tools/ast-grep/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { CLI_LANGUAGES, NAPI_LANGUAGES } from "./constants"
|
||||
|
||||
export type CliLanguage = (typeof CLI_LANGUAGES)[number]
|
||||
export type NapiLanguage = (typeof NAPI_LANGUAGES)[number]
|
||||
|
||||
export interface Position {
|
||||
line: number
|
||||
column: number
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
start: Position
|
||||
end: Position
|
||||
}
|
||||
|
||||
export interface CliMatch {
|
||||
text: string
|
||||
range: {
|
||||
byteOffset: { start: number; end: number }
|
||||
start: Position
|
||||
end: Position
|
||||
}
|
||||
file: string
|
||||
lines: string
|
||||
charCount: { leading: number; trailing: number }
|
||||
language: string
|
||||
}
|
||||
|
||||
export interface SearchMatch {
|
||||
file: string
|
||||
text: string
|
||||
range: Range
|
||||
lines: string
|
||||
}
|
||||
|
||||
export interface MetaVariable {
|
||||
name: string
|
||||
text: string
|
||||
kind: string
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
text: string
|
||||
range: Range
|
||||
kind: string
|
||||
metaVariables: MetaVariable[]
|
||||
}
|
||||
|
||||
export interface TransformResult {
|
||||
original: string
|
||||
transformed: string
|
||||
editCount: number
|
||||
}
|
||||
72
src/tools/ast-grep/utils.ts
Normal file
72
src/tools/ast-grep/utils.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { CliMatch, AnalyzeResult } from "./types"
|
||||
|
||||
export function formatSearchResult(matches: CliMatch[]): string {
|
||||
if (matches.length === 0) {
|
||||
return "No matches found"
|
||||
}
|
||||
|
||||
const lines: string[] = [`Found ${matches.length} match(es):\n`]
|
||||
|
||||
for (const match of matches) {
|
||||
const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`
|
||||
lines.push(`${loc}`)
|
||||
lines.push(` ${match.lines.trim()}`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export function formatReplaceResult(matches: CliMatch[], isDryRun: boolean): string {
|
||||
if (matches.length === 0) {
|
||||
return "No matches found to replace"
|
||||
}
|
||||
|
||||
const prefix = isDryRun ? "[DRY RUN] " : ""
|
||||
const lines: string[] = [`${prefix}${matches.length} replacement(s):\n`]
|
||||
|
||||
for (const match of matches) {
|
||||
const loc = `${match.file}:${match.range.start.line + 1}:${match.range.start.column + 1}`
|
||||
lines.push(`${loc}`)
|
||||
lines.push(` ${match.text}`)
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
lines.push("Use dryRun=false to apply changes")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export function formatAnalyzeResult(results: AnalyzeResult[], extractedMetaVars: boolean): string {
|
||||
if (results.length === 0) {
|
||||
return "No matches found"
|
||||
}
|
||||
|
||||
const lines: string[] = [`Found ${results.length} match(es):\n`]
|
||||
|
||||
for (const result of results) {
|
||||
const loc = `L${result.range.start.line + 1}:${result.range.start.column + 1}`
|
||||
lines.push(`[${loc}] (${result.kind})`)
|
||||
lines.push(` ${result.text}`)
|
||||
|
||||
if (extractedMetaVars && result.metaVariables.length > 0) {
|
||||
lines.push(" Meta-variables:")
|
||||
for (const mv of result.metaVariables) {
|
||||
lines.push(` $${mv.name} = "${mv.text}" (${mv.kind})`)
|
||||
}
|
||||
}
|
||||
lines.push("")
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
export function formatTransformResult(original: string, transformed: string, editCount: number): string {
|
||||
if (editCount === 0) {
|
||||
return "No matches found to transform"
|
||||
}
|
||||
|
||||
return `Transformed (${editCount} edit(s)):\n\`\`\`\n${transformed}\n\`\`\``
|
||||
}
|
||||
Reference in New Issue
Block a user