From e7ad2b9817c377cd16f11bed549c9198358e9685 Mon Sep 17 00:00:00 2001 From: auyua9 Date: Sat, 25 Apr 2026 11:39:07 +0800 Subject: [PATCH] fix(ast-grep): restore pattern hints in tools --- src/tools/ast-grep/tools.test.ts | 55 ++++++++++++++++++++++++++++++++ src/tools/ast-grep/tools.ts | 45 ++++++-------------------- 2 files changed, 65 insertions(+), 35 deletions(-) create mode 100644 src/tools/ast-grep/tools.test.ts diff --git a/src/tools/ast-grep/tools.test.ts b/src/tools/ast-grep/tools.test.ts new file mode 100644 index 000000000..b02fa8ea4 --- /dev/null +++ b/src/tools/ast-grep/tools.test.ts @@ -0,0 +1,55 @@ +/// + +import { beforeEach, describe, expect, it, mock } from "bun:test" +import { AST_GREP_REPLACE_DESCRIPTION, AST_GREP_SEARCH_DESCRIPTION } from "./tool-descriptions" + +const runSgMock = mock(async () => ({ + matches: [], + totalMatches: 0, + truncated: false, +})) + +mock.module("./cli", () => ({ + runSg: runSgMock, +})) + +import { createAstGrepTools } from "./tools" + +describe("createAstGrepTools", () => { + beforeEach(() => { + runSgMock.mockClear() + }) + + it("#given the production tool factory #when creating tools #then exposes shared ast-grep descriptions", () => { + // given / when + const tools = createAstGrepTools({ directory: "/repo" } as never) + + // then + expect(tools.ast_grep_search.description).toBe(AST_GREP_SEARCH_DESCRIPTION) + expect(tools.ast_grep_replace.description).toBe(AST_GREP_REPLACE_DESCRIPTION) + expect(tools.ast_grep_search.description).toContain("NOT regex") + }) + + it("#given empty search results from a regex-shaped pattern #when executing #then appends the pattern hint", async () => { + // given + const tools = createAstGrepTools({ directory: "/repo" } as never) + + // when + const output = await tools.ast_grep_search.execute( + { pattern: "foo|bar", lang: "typescript" }, + {}, + ) + + // then + expect(output).toContain("No matches found") + expect(output).toContain("alternation") + expect(output).toContain("grep") + expect(runSgMock).toHaveBeenCalledWith({ + pattern: "foo|bar", + lang: "typescript", + paths: ["/repo"], + globs: undefined, + context: undefined, + }) + }) +}) diff --git a/src/tools/ast-grep/tools.ts b/src/tools/ast-grep/tools.ts index 98b2d0c7e..2a2454fd2 100644 --- a/src/tools/ast-grep/tools.ts +++ b/src/tools/ast-grep/tools.ts @@ -3,6 +3,12 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { CLI_LANGUAGES } from "./constants" import { runSg } from "./cli" import { formatSearchResult, formatReplaceResult } from "./result-formatter" +import { getPatternHint } from "./pattern-hints" +import { + AST_GREP_REPLACE_DESCRIPTION, + AST_GREP_SEARCH_DESCRIPTION, + AST_GREP_SEARCH_PATTERN_PARAM, +} from "./tool-descriptions" import type { CliLanguage } from "./types" async function showOutputToUser(context: unknown, output: string): Promise { @@ -12,39 +18,11 @@ async function showOutputToUser(context: unknown, output: string): Promise await ctx.metadata?.({ metadata: { output } }) } -function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null { - const src = pattern.trim() - - if (lang === "python") { - if (src.startsWith("class ") && src.endsWith(":")) { - const withoutColon = src.slice(0, -1) - return `Hint: Remove trailing colon. Try: "${withoutColon}"` - } - if ((src.startsWith("def ") || src.startsWith("async def ")) && src.endsWith(":")) { - const withoutColon = src.slice(0, -1) - return `Hint: Remove trailing colon. Try: "${withoutColon}"` - } - } - - if (["javascript", "typescript", "tsx"].includes(lang)) { - if (/^(export\s+)?(async\s+)?function\s+\$[A-Z_]+\s*$/i.test(src)) { - return `Hint: Function patterns need params and body. Try "function $NAME($$$) { $$$ }"` - } - } - - return null -} - export function createAstGrepTools(ctx: PluginInput): Record { const ast_grep_search: ToolDefinition = tool({ - description: - "Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " + - "Use meta-variables: $VAR (single node), $$$ (multiple nodes). " + - "IMPORTANT: Patterns must be complete AST nodes (valid code). " + - "For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " + - "Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'", + description: AST_GREP_SEARCH_DESCRIPTION, args: { - pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."), + pattern: tool.schema.string().describe(AST_GREP_SEARCH_PATTERN_PARAM), 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)"), @@ -63,7 +41,7 @@ export function createAstGrepTools(ctx: PluginInput): Record