Skip to content

Commit 2464473

Browse files
authored
fix(ast-grep): add validation for incomplete function declaration patterns (#5)
* fix(publish): make git operations idempotent - Check for staged changes before commit - Check if tag exists before creating - Check if release exists before creating * fix(ast-grep): add validation for incomplete function declaration patterns - Add validatePatternForCli function to detect incomplete patterns like 'export async function $METHOD' (missing params and body) - Only validates JS/TS languages (javascript, typescript, tsx) - Provides helpful error message with correct pattern examples - Update tool description to clarify complete AST nodes required This fixes the issue where incomplete patterns would fail silently with no results instead of providing actionable feedback.
1 parent 1b0a8ad commit 2464473

File tree

2 files changed

+70
-4
lines changed

2 files changed

+70
-4
lines changed

script/publish.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,35 @@ async function gitTagAndRelease(newVersion: string, changelog: string): Promise<
7878
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
7979
await $`git config user.name "github-actions[bot]"`
8080
await $`git add package.json`
81-
await $`git commit -m "release: v${newVersion}"`
82-
await $`git tag v${newVersion}`
81+
82+
// Commit only if there are staged changes (idempotent)
83+
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow()
84+
if (hasStagedChanges.exitCode !== 0) {
85+
await $`git commit -m "release: v${newVersion}"`
86+
} else {
87+
console.log("No changes to commit (version already updated)")
88+
}
89+
90+
// Tag only if it doesn't exist (idempotent)
91+
const tagExists = await $`git rev-parse v${newVersion}`.nothrow()
92+
if (tagExists.exitCode !== 0) {
93+
await $`git tag v${newVersion}`
94+
} else {
95+
console.log(`Tag v${newVersion} already exists`)
96+
}
97+
98+
// Push (idempotent - git push is already idempotent)
8399
await $`git push origin HEAD --tags`
84100

101+
// Create release only if it doesn't exist (idempotent)
85102
console.log("\nCreating GitHub release...")
86103
const releaseNotes = changelog || "No notable changes"
87-
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`
104+
const releaseExists = await $`gh release view v${newVersion}`.nothrow()
105+
if (releaseExists.exitCode !== 0) {
106+
await $`gh release create v${newVersion} --title "v${newVersion}" --notes ${releaseNotes}`
107+
} else {
108+
console.log(`Release v${newVersion} already exists`)
109+
}
88110
}
89111

90112
async function checkVersionExists(version: string): Promise<boolean> {

src/tools/ast-grep/tools.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,64 @@ function showOutputToUser(context: unknown, output: string): void {
1010
ctx.metadata?.({ metadata: { output } })
1111
}
1212

13+
/**
14+
* JS/TS languages that require complete function declaration patterns
15+
*/
16+
const JS_TS_LANGUAGES = ["javascript", "typescript", "tsx"] as const
17+
18+
/**
19+
* Validates AST pattern for common incomplete patterns that will fail silently.
20+
* Only validates JS/TS languages where function declarations require body.
21+
*
22+
* @throws Error with helpful message if pattern is incomplete
23+
*/
24+
function validatePatternForCli(pattern: string, lang: CliLanguage): void {
25+
if (!JS_TS_LANGUAGES.includes(lang as (typeof JS_TS_LANGUAGES)[number])) {
26+
return
27+
}
28+
29+
const src = pattern.trim()
30+
31+
// Detect incomplete function declarations:
32+
// - "function $NAME" (no params/body)
33+
// - "export function $NAME" (no params/body)
34+
// - "export async function $NAME" (no params/body)
35+
// - "export default function $NAME" (no params/body)
36+
// Pattern: ends with $METAVAR (uppercase, underscore, digits) without ( or {
37+
const incompleteFunctionDecl =
38+
/^(export\s+)?(default\s+)?(async\s+)?function\s+\$[A-Z_][A-Z0-9_]*\s*$/i.test(src)
39+
40+
if (incompleteFunctionDecl) {
41+
throw new Error(
42+
`Incomplete AST pattern for ${lang}: "${pattern}"\n\n` +
43+
`ast-grep requires complete AST nodes. Function declarations must include parameters and body.\n\n` +
44+
`Examples of correct patterns:\n` +
45+
` - "export async function $NAME($$$) { $$$ }" (matches export async functions)\n` +
46+
` - "function $NAME($$$) { $$$ }" (matches all function declarations)\n` +
47+
` - "async function $NAME($$$) { $$$ }" (matches async functions)\n\n` +
48+
`Your pattern "${pattern}" is missing the parameter list and body.`
49+
)
50+
}
51+
}
52+
1353
export const ast_grep_search = tool({
1454
description:
1555
"Search code patterns across filesystem using AST-aware matching. Supports 25 languages. " +
1656
"Use meta-variables: $VAR (single node), $$$ (multiple nodes). " +
57+
"IMPORTANT: Patterns must be complete AST nodes (valid code). " +
58+
"For functions, include params and body: 'export async function $NAME($$$) { $$$ }' not 'export async function $NAME'. " +
1759
"Examples: 'console.log($MSG)', 'def $FUNC($$$):', 'async function $NAME($$$)'",
1860
args: {
19-
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$)"),
61+
pattern: tool.schema.string().describe("AST pattern with meta-variables ($VAR, $$$). Must be complete AST node."),
2062
lang: tool.schema.enum(CLI_LANGUAGES).describe("Target language"),
2163
paths: tool.schema.array(tool.schema.string()).optional().describe("Paths to search (default: ['.'])"),
2264
globs: tool.schema.array(tool.schema.string()).optional().describe("Include/exclude globs (prefix ! to exclude)"),
2365
context: tool.schema.number().optional().describe("Context lines around match"),
2466
},
2567
execute: async (args, context) => {
2668
try {
69+
validatePatternForCli(args.pattern, args.lang as CliLanguage)
70+
2771
const matches = await runSg({
2872
pattern: args.pattern,
2973
lang: args.lang as CliLanguage,

0 commit comments

Comments
 (0)