feat(ast-grep): rewrite tool descriptions to prevent regex-style misuse
The previous description (41 words) told the LLM to write 'complete AST nodes' but did not explain that regex syntax is the #1 failure mode. It also shipped a bug: the Python example 'def $FUNC($$$):' had a trailing colon that the hint system actively flags as wrong. Extract descriptions into tool-descriptions.ts and rewrite: - Open with 'This is NOT regex' so the constraint is unmissable - List the four regex patterns that do not work (|, .*, \\w, [a-z]) with the corrective action for each - Tell the LLM to switch to grep when the pattern is text-shaped - Fix the Python example (no trailing colon) and add Go and Rust rows since the failing reports came from Go codebases - Shorten the pattern-param description with the same anti-regex list Also harden the LSP reference for the new test files using the bun-types triple-slash directive already used elsewhere.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
detectLanguageSpecificMistake,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
AST_GREP_REPLACE_DESCRIPTION,
|
||||
AST_GREP_SEARCH_DESCRIPTION,
|
||||
AST_GREP_SEARCH_PATTERN_PARAM,
|
||||
} from "./tool-descriptions"
|
||||
|
||||
describe("AST_GREP_SEARCH_DESCRIPTION", () => {
|
||||
it("#given the description #when inspecting #then asserts it is NOT regex", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("NOT regex")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then explains meta-variables $VAR and $$$", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("$VAR")
|
||||
expect(description).toContain("$$$")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then warns against regex alternation", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("alternation")
|
||||
expect(description).toContain("|")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then warns against regex wildcards", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain(".*")
|
||||
expect(description).toContain("wildcards")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then warns against regex escapes", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("\\w")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then warns against character classes", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("[a-z]")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then tells LLM to use grep as fallback", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description.toLowerCase()).toContain("grep")
|
||||
})
|
||||
|
||||
it("#given the description #when showing Python example #then omits the trailing colon bug", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).not.toContain("def $FUNC($$$):")
|
||||
expect(description).toContain("def $FUNC($$$)")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then shows TypeScript example", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("typescript")
|
||||
expect(description).toContain("function $NAME($$$) { $$$ }")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then shows Go example", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("go")
|
||||
expect(description).toContain("func $NAME($$$) { $$$ }")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then shows Rust example", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("rust")
|
||||
expect(description).toContain("fn $NAME(")
|
||||
})
|
||||
|
||||
it("#given the description #when measuring #then stays within a token-reasonable length", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description.length).toBeLessThan(2000)
|
||||
expect(description.length).toBeGreaterThan(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe("AST_GREP_SEARCH_PATTERN_PARAM", () => {
|
||||
it("#given the param description #when inspecting #then states meta-var rules", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_PATTERN_PARAM
|
||||
|
||||
// then
|
||||
expect(description).toContain("$VAR")
|
||||
expect(description).toContain("$$$")
|
||||
})
|
||||
|
||||
it("#given the param description #when inspecting #then forbids regex syntax", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_PATTERN_PARAM
|
||||
|
||||
// then
|
||||
expect(description).toContain("NOT regex")
|
||||
expect(description).toContain("|")
|
||||
expect(description).toContain(".*")
|
||||
})
|
||||
|
||||
it("#given the param description #when inspecting #then directs to grep for fallback", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_SEARCH_PATTERN_PARAM
|
||||
|
||||
// then
|
||||
expect(description.toLowerCase()).toContain("grep")
|
||||
})
|
||||
})
|
||||
|
||||
describe("AST_GREP_REPLACE_DESCRIPTION", () => {
|
||||
it("#given the description #when inspecting #then mentions AST meta-variables", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_REPLACE_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("$VAR")
|
||||
expect(description).toContain("$$$")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then warns against regex", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_REPLACE_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description.toLowerCase()).toContain("regex does not work")
|
||||
})
|
||||
|
||||
it("#given the description #when inspecting #then provides an example", () => {
|
||||
// given / when
|
||||
const description = AST_GREP_REPLACE_DESCRIPTION
|
||||
|
||||
// then
|
||||
expect(description).toContain("console.log($MSG)")
|
||||
expect(description).toContain("logger.info($MSG)")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
export const AST_GREP_SEARCH_DESCRIPTION = [
|
||||
"Search code by AST structure (25 languages). This is NOT regex.",
|
||||
"",
|
||||
"Meta-variables (the only wildcards ast-grep understands):",
|
||||
" $VAR - one AST node (an identifier, expression, statement, ...)",
|
||||
" $$$ - zero or more nodes (argument lists, function bodies, ...)",
|
||||
" $$$VAR - same, captured by name",
|
||||
"Patterns must be complete, parseable source code. Each meta-variable replaces a whole node, not a substring.",
|
||||
"",
|
||||
"Regex syntax does NOT work - never pass these to pattern:",
|
||||
' "foo|bar" alternation → run separate calls, or switch to grep',
|
||||
' ".*", ".+" wildcards → use $$$ between AST fragments',
|
||||
' "\\w", "\\d" escapes → use $VAR to capture any identifier',
|
||||
' "[a-z]" class ranges → no AST equivalent',
|
||||
"For text search, cross-language search, or regex features, use the grep tool instead.",
|
||||
"",
|
||||
"Examples by language:",
|
||||
' typescript/tsx "function $NAME($$$) { $$$ }", "console.log($$$)", "import { $$$ } from \'$MOD\'"',
|
||||
' python "def $FUNC($$$)", "class $C($$$)" - no trailing colon',
|
||||
' go "func $NAME($$$) { $$$ }", "if err != nil { $$$ }"',
|
||||
' rust "fn $NAME($$$) -> $RET { $$$ }", "impl $TRAIT for $T { $$$ }"',
|
||||
"",
|
||||
"On empty results the tool returns a hint naming the exact mistake. If the pattern is fundamentally text-shaped, stop retrying and switch to grep.",
|
||||
].join("\n")
|
||||
|
||||
export const AST_GREP_SEARCH_PATTERN_PARAM =
|
||||
"AST pattern - valid, parseable code using $VAR (one node) and $$$ (many nodes). NOT regex: no `|`, no `.*`, no `\\w`, no `[a-z]`. For text or alternation, use grep instead."
|
||||
|
||||
export const AST_GREP_REPLACE_DESCRIPTION = [
|
||||
"Rewrite code by AST pattern (25 languages). Dry-run by default.",
|
||||
"Both pattern and rewrite use AST syntax ($VAR for one node, $$$ for many) - regex does NOT work.",
|
||||
"Meta-variables captured in pattern can be reused in rewrite to preserve matched content.",
|
||||
'Example: pattern="console.log($MSG)" rewrite="logger.info($MSG)"',
|
||||
"For text-only replacement or regex features, use a text editor instead.",
|
||||
].join("\n")
|
||||
@@ -4,6 +4,11 @@ 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<void> {
|
||||
@@ -15,14 +20,9 @@ async function showOutputToUser(context: unknown, output: string): Promise<void>
|
||||
|
||||
export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinition> {
|
||||
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)"),
|
||||
@@ -58,10 +58,7 @@ export function createAstGrepTools(ctx: PluginInput): Record<string, ToolDefinit
|
||||
})
|
||||
|
||||
const ast_grep_replace: ToolDefinition = 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)'",
|
||||
description: AST_GREP_REPLACE_DESCRIPTION,
|
||||
args: {
|
||||
pattern: tool.schema.string().describe("AST pattern to match"),
|
||||
rewrite: tool.schema.string().describe("Replacement pattern (can use $VAR from pattern)"),
|
||||
|
||||
Reference in New Issue
Block a user