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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user