fix(ast): allow empty replacements and stop trimming patterns/rewrites

Remove the non-empty validation on `out` in both the TypeScript tool
layer and the Rust native layer. An empty replacement template now
deletes the matched AST node, which is the natural semantics.

Also remove .trim() calls on pattern and rewrite strings — trimming
silently destroys intentional whitespace and serves no purpose since
ast-grep matches on AST structure, not raw text.
This commit is contained in:
can1357
2026-02-28 21:13:59 +01:00
parent 924a70d9b4
commit 02dd71d847
3 changed files with 7 additions and 20 deletions
+2 -9
View File
@@ -517,20 +517,13 @@ fn normalize_rewrite_map(
rewrites: Option<HashMap<String, String>>,
) -> Result<Vec<(String, String)>> {
let mut normalized = Vec::new();
for (raw_pattern, raw_rewrite) in rewrites.unwrap_or_default() {
let pattern = raw_pattern.trim();
for (pattern, rewrite) in rewrites.unwrap_or_default() {
if pattern.is_empty() {
return Err(Error::from_reason(
"`rewrites` keys must be non-empty pattern strings".to_string(),
));
}
let rewrite = raw_rewrite.trim();
if rewrite.is_empty() {
return Err(Error::from_reason(format!(
"`rewrites[{pattern}]` must be a non-empty rewrite template"
)));
}
normalized.push((pattern.to_string(), rewrite.to_string()));
normalized.push((pattern, rewrite));
}
if normalized.is_empty() {
return Err(Error::from_reason(
+2
View File
@@ -20,6 +20,8 @@
### Fixed
- `ast_replace` no longer rejects empty `out` values; an empty string now deletes matched nodes
- `ast_replace` no longer trims `pat` and `out` values, preserving intentional whitespace
- `gemini_image` tool: corrected `responseModalities` values from `'Image'`/`'Text'` to uppercase `'IMAGE'`/`'TEXT'` matching the API enum
### Breaking Changes
+3 -11
View File
@@ -69,15 +69,10 @@ export class AstReplaceTool implements AgentTool<typeof astReplaceSchema, AstRep
): Promise<AgentToolResult<AstReplaceToolDetails>> {
return untilAborted(signal, async () => {
const ops = params.ops.map((entry, index) => {
const pat = entry.pat.trim();
const out = entry.out.trim();
if (pat.length === 0) {
if (entry.pat.length === 0) {
throw new ToolError(`\`ops[${index}].pat\` must be a non-empty pattern`);
}
if (out.length === 0) {
throw new ToolError(`\`ops[${index}].out\` must be a non-empty replacement template`);
}
return [pat, out] as const;
return [entry.pat, entry.out] as const;
});
if (ops.length === 0) {
throw new ToolError("`ops` must include at least one op entry");
@@ -122,10 +117,7 @@ export class AstReplaceTool implements AgentTool<typeof astReplaceSchema, AstRep
}
const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
const scopePath = (() => {
const relative = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/");
return relative.length === 0 ? "." : relative;
})();
const scopePath = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/") || ".";
let isDirectory: boolean;
try {
const stat = await Bun.file(resolvedSearchPath).stat();