pub import { edit_capabilities } from "std/edit/capabilities"
pub import { edit_fast_apply, fast_apply } from "std/edit/fast_apply"
pub import {
edit_apply_node,
edit_apply_old_new_patch,
edit_batch_apply,
edit_changed_regions,
edit_check_lazy_truncation,
edit_explain_whitespace_difference,
edit_insert_at_anchor,
edit_splice_lines,
edit_strip_line_number_prefixes,
edit_validate_changed_regions,
} from "std/edit/patch"
import {
__refactor_conflict,
__refactor_invalid,
__refactor_run,
__refactor_unsupported,
} from "std/edit/refactor_runtime"
pub import {
edit_add_parameter,
edit_change_signature,
edit_dry_run,
edit_rename_symbol,
edit_reorder_parameters,
edit_safe_text_patch,
} from "std/edit/safe_patch"
/**
* Structured refactorings (B.8, issue #2520).
*
* Higher-level, compound, language-aware edits composed on top of the
* B.1-B.5 primitives above (`edit_apply_node`, `edit_insert_at_anchor`,
* `edit_rename_symbol`, `edit_safe_text_patch`, `edit_dry_run`) plus the
* host AST analyzers (`ast.symbols`, `ast.function_body`,
* `ast.undefined_names`).
*
* Every refactoring shares one driver, `__refactor_run`, which lowers a
* list of typed ops into staged-fs writes (#1722): a `dry_run` previews
* the change as a per-file unified diff against a throw-away overlay and
* touches no bytes, while an apply stages all ops into one session and
* commits them atomically (or discards on the first conflict). Callers
* that already own a staged session pass `session_id`; the driver then
* stages into it and leaves the commit to the caller.
*
* Each public entry point first consults a per-(operation, language)
* capability matrix and returns `result: "unsupported"` rather than
* guessing when a language lacks the structure a refactoring needs.
*
* All of these route through gated host builtins (`apply_node`,
* `insert_at_anchor`, `read_text`, `safe_text_patch`), so callers must
* hold the `tools:deterministic` capability.
*/
fn __refactor_normalize_lang(name: string?) {
const n = lowercase(trim(name ?? ""))
const alias = {
golang: "go",
js: "javascript",
py: "python",
rb: "ruby",
rs: "rust",
ts: "typescript",
}
return alias.get(n, n)
}
fn __refactor_detect_language(params: dict) {
const hint = params?.language
if hint != nil && hint != "" {
return __refactor_normalize_lang(hint)
}
const path = params?.path ?? ""
const segments = split(path, "/")
const filename = if len(segments) > 0 {
segments[-1]
} else {
path
}
const dotted = split(filename, ".")
if len(dotted) < 2 {
return ""
}
return __refactor_normalize_lang(dotted[-1])
}
fn __refactor_leading_ws(line: string?) {
const raw = line ?? ""
const body = regex_replace("^[ \\t]+", "", raw)
return raw.substring(0, len(raw) - len(body))
}
fn __refactor_var_decl_syntax(language: any) {
const table = {
go: {kw: "", assign: " := ", term: ""},
javascript: {kw: "const ", assign: " = ", term: ";"},
jsx: {kw: "const ", assign: " = ", term: ";"},
python: {kw: "", assign: " = ", term: ""},
ruby: {kw: "", assign: " = ", term: ""},
rust: {kw: "let ", assign: " = ", term: ";"},
swift: {kw: "let ", assign: " = ", term: ""},
tsx: {kw: "const ", assign: " = ", term: ";"},
typescript: {kw: "const ", assign: " = ", term: ";"},
}
return table.get(language, nil)
}
/**
* Extract a single-line expression range into a freshly-declared local
* binding, replacing the original span with `new_name` and inserting the
* declaration on its own line directly above, at the same indentation.
*
* `range` is `{start_line, start_col, end_line, end_col}` in 0-based
* tree-sitter coordinates (columns are byte offsets within the line).
* Multi-line ranges return `result: "unsupported"` — select an
* expression that lives on one line.
*
* The declaration form is chosen from a per-language table (`let`/`const`/
* `:=`/bare), so languages that need an explicit type to declare a local
* (Java, C, C++) return `result: "unsupported"`.
*
* Shares the staged-fs preview/atomic-apply contract of the other
* structured refactorings: pass `dry_run: true` for a unified-diff
* preview, `session_id` to stage into a caller-owned session.
*
* @effects: [host, fs]
* @errors: [backend]
* @api_stability: experimental
* @example: edit_extract_variable({path: "src/lib.rs", range: {start_line: 4, start_col: 13, end_line: 4, end_col: 24}, new_name: "total"})
*/
pub fn edit_extract_variable(fs: HarnessFs, random: HarnessRandom, ast: HarnessAst, params: dict?) {
const p = params ?? {}
const language = __refactor_detect_language(p)
const syntax = __refactor_var_decl_syntax(language)
if syntax == nil {
return __refactor_unsupported(
"extract_variable",
language,
"extract_variable has no local-declaration form for language `" + language + "`",
)
}
const {path = "", new_name = "", range = {}} = p ?? {}
if path == "" || new_name == "" {
return __refactor_invalid("extract_variable", language, "`path` and `new_name` are required")
}
const start_line = range?.start_line
const {end_line = start_line, start_col = 0} = range ?? {}
const end_col = range?.end_col
if start_line == nil || end_col == nil {
return __refactor_invalid(
"extract_variable",
language,
"`range` must carry start_line, start_col, end_line, end_col",
)
}
if end_line != start_line {
return __refactor_unsupported(
"extract_variable",
language,
"multi-line ranges are not supported; select an expression on one line",
)
}
const read = fs.staged_read_text({path: path, session_id: p?.session_id})
const content = read?.content ?? ""
const lines = split(content, "\n")
if start_line < 0 || start_line >= len(lines) {
return __refactor_invalid("extract_variable", language, "start_line is outside the file")
}
const line = lines[start_line]
const expr = line.substring(start_col, end_col)
if trim(expr) == "" {
return __refactor_invalid("extract_variable", language, "selected range is empty or whitespace")
}
const indent = __refactor_leading_ws(line)
const decl = indent + syntax.kw + new_name + syntax.assign + trim(expr) + syntax.term
const replaced_line = line.substring(0, start_col) + new_name + line.substring(end_col)
let new_lines = if start_line > 0 {
lines[:start_line]
} else {
[]
}
new_lines = new_lines + [decl, replaced_line]
if start_line + 1 < len(lines) {
new_lines = new_lines + lines[start_line + 1:]
}
const after = join(new_lines, "\n")
return __refactor_run(
fs,
random,
ast,
"extract_variable",
language,
[{kind: "content", path: path, after: after}],
p,
[],
)
}
/**
* --- Signature-family + return-type refactorings ----------------------------
*
* Tree-sitter node/field names per language were verified empirically: the
* `parameters` field holds the param-list node (whose node type varies:
* `parameters` / `formal_parameters` / `parameter_list`); the return-type
* field is `return_type` (Rust/Python/TS) or `result` (Go); call argument
* lists are the `arguments` / `argument_list` node under a call node's
* `function` field. Replacing a params or args node replaces the enclosing
* parens too, so replacements re-add them. TS return-type nodes include the
* leading `: `, so `ret_prefix` re-adds it.
*/
fn __refactor_lang_spec(language: any) {
const table = {
go: {
fn_node: "function_declaration",
params_node: "parameter_list",
ret_field: "result",
ret_node: "(_)",
ret_prefix: "",
call_node: "call_expression",
args_node: "argument_list",
},
javascript: {
fn_node: "function_declaration",
params_node: "formal_parameters",
ret_field: nil,
ret_node: nil,
ret_prefix: "",
call_node: "call_expression",
args_node: "arguments",
},
jsx: {
fn_node: "function_declaration",
params_node: "formal_parameters",
ret_field: nil,
ret_node: nil,
ret_prefix: "",
call_node: "call_expression",
args_node: "arguments",
},
python: {
fn_node: "function_definition",
params_node: "parameters",
ret_field: "return_type",
ret_node: "(type)",
ret_prefix: "",
call_node: "call",
args_node: "argument_list",
},
rust: {
fn_node: "function_item",
params_node: "parameters",
ret_field: "return_type",
ret_node: "(_)",
ret_prefix: "",
call_node: "call_expression",
args_node: "arguments",
},
tsx: {
fn_node: "function_declaration",
params_node: "formal_parameters",
ret_field: "return_type",
ret_node: "(type_annotation)",
ret_prefix: ": ",
call_node: "call_expression",
args_node: "arguments",
},
typescript: {
fn_node: "function_declaration",
params_node: "formal_parameters",
ret_field: "return_type",
ret_node: "(type_annotation)",
ret_prefix: ": ",
call_node: "call_expression",
args_node: "arguments",
},
}
return table.get(language, nil)
}
fn __refactor_symbol_name(p: dict) {
const s = p?.symbol
if type_of(s) == "dict" {
return s?.name ?? ""
}
if type_of(s) == "string" {
return s
}
return p?.name ?? ""
}
fn __refactor_paren_inner(s: string?) {
const raw = s ?? ""
if len(raw) < 2 {
return ""
}
return raw.substring(1, len(raw) - 1)
}
fn __refactor_split_top_level(text: string?) {
let segs = []
let depth = 0
let quote = ""
let cur = ""
for c in (text ?? "").chars() {
if quote != "" {
cur = cur + c
if c == quote {
quote = ""
}
} else if c == "\"" || c == "'" {
quote = c
cur = cur + c
} else if c == "(" || c == "[" || c == "{" {
depth = depth + 1
cur = cur + c
} else if c == ")" || c == "]" || c == "}" {
depth = depth - 1
cur = cur + c
} else if c == "," && depth == 0 {
if trim(cur) != "" {
segs = segs + [trim(cur)]
}
cur = ""
} else {
cur = cur + c
}
}
if trim(cur) != "" {
segs = segs + [trim(cur)]
}
return segs
}
fn __refactor_def_query(spec: dict, fn_name: any) {
return "(" + spec.fn_node + " name: (identifier) @__n parameters: (" + spec.params_node
+ ") @target (#eq? @__n \""
+ fn_name
+ "\"))"
}
fn __refactor_call_query(spec: dict, fn_name: any) {
return "(" + spec.call_node + " function: (identifier) @__f arguments: (" + spec.args_node
+ ") @target (#eq? @__f \""
+ fn_name
+ "\"))"
}
fn __refactor_probe(ast: HarnessAst, query: any, path: any, session_id: any) {
return edit_apply_node(
ast,
{
path: path,
query: query,
replacement: "",
select: "all",
dry_run: true,
validate: false,
session_id: session_id,
},
)
}
fn __refactor_read_def_params(
ast: HarnessAst,
spec: dict,
path: any,
fn_name: any,
session_id: any,
) {
const r = edit_apply_node(
ast,
{
path: path,
query: __refactor_def_query(spec, fn_name),
replacement: "",
select: "unique",
dry_run: true,
validate: false,
session_id: session_id,
},
)
if r.result != "applied" {
return {ok: false, result: r.result, inner: "", params: []}
}
const original = if len(r.edits) > 0 {
r.edits[0].original
} else {
"()"
}
const inner = __refactor_paren_inner(original)
return {ok: true, result: "applied", inner: inner, params: __refactor_split_top_level(inner)}
}
fn __refactor_call_sites(ast: HarnessAst, spec: dict, path: any, fn_name: any, session_id: any) {
const r = __refactor_probe(ast, __refactor_call_query(spec, fn_name), path, session_id)
if r.result == "applied" {
return {count: r.match_count, originals: r.edits.map({ e -> e.original })}
}
return {count: 0, originals: []}
}
/**
* Replace a function's declared return type with `new_type`. Languages
* that carry a return-type slot are supported (rust, python, typescript,
* tsx, go); JavaScript/JSX have no annotation and return
* `result: "unsupported"`. A function with no existing return-type
* annotation surfaces as `result: "conflict"` (nothing to replace).
*
* Caller verification is currently syntactic only: the rewritten file is
* re-parsed (tree-sitter) but callers are not type-checked. The count of
* call sites is surfaced in `warnings` so the agent knows what to review.
*
* @effects: [host, fs]
* @errors: [backend]
* @api_stability: experimental
* @example: edit_change_return_type({path: "src/lib.rs", symbol: {name: "add"}, new_type: "i32"})
*/
pub fn edit_change_return_type(
fs: HarnessFs,
random: HarnessRandom,
ast: HarnessAst,
params: dict?,
) {
const p = params ?? {}
const language = __refactor_detect_language(p)
const spec = __refactor_lang_spec(language)
if spec == nil || spec.ret_field == nil {
return __refactor_unsupported(
"change_return_type",
language,
"language `" + language + "` has no return-type annotation to rewrite",
)
}
const path = p?.path ?? ""
const fn_name = __refactor_symbol_name(p)
const new_type = p?.new_type
if path == "" || fn_name == "" || new_type == nil || new_type == "" {
return __refactor_invalid(
"change_return_type",
language,
"`path`, `symbol`/`name`, and `new_type` are required",
)
}
const probe_session = if p?.dry_run ?? false {
nil
} else {
p?.session_id
}
const query = "(" + spec.fn_node + " name: (identifier) @__n " + spec.ret_field + ": "
+ spec.ret_node
+ " @target (#eq? @__n \""
+ fn_name
+ "\"))"
const op = {
kind: "node_apply",
path: path,
params: {path: path, query: query, replacement: spec.ret_prefix + new_type, select: "unique"},
}
const calls = __refactor_call_sites(ast, spec, path, fn_name, probe_session)
const warns = if calls.count > 0 {
[
{
code: "callers_not_type_checked",
message: to_string(calls.count)
+ " call site(s) were re-parsed but not type-checked against the new return type",
},
]
} else {
[]
}
return __refactor_run(fs, random, ast, "change_return_type", language, [op], p, warns)
}
/**
* --- extract_function -------------------------------------------------------
* Extract an expression, a run of statements, or a closure body into a new
* function `new_name`, replacing it and every same-file copy with a call.
*
* A thin wrapper over `code_index.extract_function`, which owns the analysis:
* the region's free names bound in the enclosing function become parameters,
* names it assigns and the caller reads afterwards become the return value
* (a tuple in Rust and Python, an object in TypeScript and JavaScript), and a
* closure body becomes a function over the closure's parameters.
*
* Name the region with `start_line`/`end_line` (1-based, inclusive) or its
* exact text in `region`. Rust and strict TypeScript need `signature`, the
* typed header; without it the result is `types_required` with the computed
* `inputs` and `outputs`. `helper` inserts a complete function verbatim.
* Refusals (`control_flow_escapes`, `signature_mismatch`, `name_conflict`,
* ...) leave the file byte-identical. Languages: rust, typescript, tsx,
* javascript, jsx, python.
*
* @effects: [host, fs]
* @errors: [backend]
* @api_stability: experimental
* @example: edit_extract_function(harness.code_index, {path: "app.py", start_line: 4, end_line: 6, new_name: "summarize"})
*/
pub fn edit_extract_function(code_index: HarnessCodeIndex, params: dict?) {
const payload = code_index.extract_function(params ?? {}) ?? {}
return payload
+ {ok: payload?.result == "applied" && len(payload?.failed_paths_with_reasons ?? []) == 0}
}
/** --- inline + move_decl ----------------------------------------------------- */
fn __refactor_inline_kind(language: string) {
if language == "python" {
return "python"
}
return "brace"
}
fn __refactor_inline_expr(body_text: string?, kind: string) {
const lines = split(body_text ?? "", "\n")
// `ast.function_body` returns the indented body only for Python,
// but the whole `fn …{ … }` text for brace languages — so brace bodies
// drop the signature/opening-brace line and the closing-brace line.
const inner = if kind == "python" {
lines
} else if len(lines) >= 3 {
lines[1:len(lines) - 1]
} else {
[]
}
let body = []
for l in inner {
if trim(l) != "" {
body = body + [trim(l)]
}
}
if len(body) != 1 {
return nil
}
const stmt = body[0]
if !stmt.starts_with("return ") {
return nil
}
let expr = trim(stmt.substring(7))
if expr.ends_with(";") {
expr = trim(expr.substring(0, len(expr) - 1))
}
if expr == "" {
return nil
}
return expr
}
/**
* Inline a zero-parameter function whose body is a single
* `return <expr>` statement: replace every `name()` call with
* `(<expr>)` and delete the definition. Functions with parameters, or
* with anything other than one return statement, return
* `result: "unsupported"` (the safe, precedence-correct subset — inlining
* parameterized bodies needs argument substitution that is left to a
* future iteration).
*
* Languages: rust, python, typescript, tsx, javascript, jsx, go.
*
* @effects: [host, fs]
* @errors: [backend]
* @api_stability: experimental
* @example: edit_inline({path: "src/lib.rs", symbol: {name: "answer"}})
*/
pub fn edit_inline(fs: HarnessFs, random: HarnessRandom, ast: HarnessAst, params: dict?) {
const p = params ?? {}
const language = __refactor_detect_language(p)
const spec = __refactor_lang_spec(language)
if spec == nil {
return __refactor_unsupported(
"inline",
language,
"no signature grammar for language `" + language + "`",
)
}
const path = p?.path ?? ""
const fn_name = __refactor_symbol_name(p)
if path == "" || fn_name == "" {
return __refactor_invalid("inline", language, "`path` and `symbol`/`name` are required")
}
const probe_session = if p?.dry_run ?? false {
nil
} else {
p?.session_id
}
const content = fs.staged_read_text({path: path, session_id: probe_session})?.content ?? ""
const fb = ast.function_body({source: content, language: language, function_name: fn_name})
if !(fb?.found ?? false) {
return __refactor_conflict(
"inline",
language,
[{code: "symbol_not_found", message: "no function `" + fn_name + "` found", path: path}],
)
}
const def = __refactor_read_def_params(ast, spec, path, fn_name, probe_session)
if def.ok && len(def.params) != 0 {
return __refactor_unsupported(
"inline",
language,
"inline supports zero-parameter functions only (argument substitution is not yet implemented)",
)
}
const expr = __refactor_inline_expr(fb.body_text, __refactor_inline_kind(language))
if expr == nil {
return __refactor_unsupported(
"inline",
language,
"inline requires a body of exactly one `return <expr>` statement",
)
}
const calls = __refactor_call_sites(ast, spec, path, fn_name, probe_session)
let i = 0
while i < calls.count {
if trim(__refactor_paren_inner(calls.originals[i])) != "" {
return __refactor_conflict(
"inline",
language,
[
{
code: "call_has_arguments",
message: "call site #" + to_string(i + 1)
+ " passes arguments to a zero-parameter function",
path: path,
},
],
)
}
i = i + 1
}
const call_target = "((" + spec.call_node + " function: (identifier) @__f (#eq? @__f \"" + fn_name
+ "\")) @target)"
const def_target = "((" + spec.fn_node + " name: (identifier) @__n (#eq? @__n \"" + fn_name
+ "\")) @target)"
let ops = []
if calls.count > 0 {
ops = ops
+ [
{
kind: "node_apply",
path: path,
params: {path: path, query: call_target, replacement: "(" + expr + ")", select: "all"},
},
]
}
ops = ops
+ [
{
kind: "node_apply",
path: path,
params: {path: path, query: def_target, replacement: "", select: "unique"},
},
]
return __refactor_run(fs, random, ast, "inline", language, ops, p, [])
}
/**
* Move a top-level declaration to another module and rewrite every import
* and qualified use of it. A thin wrapper over `code_index.move_symbol`
* (Rust, TypeScript/JavaScript, Python), which owns resolution, rewrites,
* refusals, and the all-or-nothing write; index the workspace with
* `code_index.rebuild` first.
*
* Pass `path`, `symbol` (a name, or `{name, line?, kind?}`), and `to_path`
* (`target_file` also works). `dry_run` returns the planned edits without
* writing. The declaration moves with its attributes or decorators and the
* comment block directly above it; `comments_left_behind` lists comment
* blocks separated from it by a blank line, which stay. Refusals
* (`no_match`, `ambiguous_symbol`, `unsupported_language`, `syntax_error`,
* `destination_conflict`, `visibility_required`, `import_cycle`, `error`)
* leave every file unchanged and name the blocking locations in `sites`.
*
* @effects: [host, fs]
* @errors: [backend]
* @api_stability: experimental
* @example: edit_move_decl(harness.code_index, {path: "src/a.rs", symbol: "helper", to_path: "src/b.rs"})
*/
pub fn edit_move_decl(code_index: HarnessCodeIndex, params: dict?) {
const p = params ?? {}
const symbol = p?.symbol
let req = p
if type_of(symbol) == "dict" {
req = p.merging(
{symbol: symbol?.name, line: p?.line ?? symbol?.line, kind: p?.kind ?? symbol?.kind},
)
}
const payload = code_index.move_symbol(req) ?? {}
const result_tag = payload?.result ?? "error"
return {
ok: result_tag == "applied" && len(payload?.failed_paths_with_reasons ?? []) == 0,
applied: payload?.applied ?? false,
result: result_tag,
dry_run: payload?.dry_run ?? false,
symbol: payload?.symbol,
to_path: payload?.to_path,
created_destination: payload?.created_destination ?? false,
touched_files: payload?.touched_files ?? [],
sites: payload?.sites ?? [],
comments_left_behind: payload?.comments_left_behind ?? [],
occurrences_replaced: payload?.occurrences_replaced ?? 0,
call_sites_updated: payload?.call_sites_updated ?? 0,
failed_paths_with_reasons: payload?.failed_paths_with_reasons ?? [],
details: payload?.details,
warnings: payload?.warnings ?? [],
provenance: {module: "std/edit", helper: "edit_move_decl", caller: p?.provenance},
}
}