normalize_refactor/inline_function.rs
1//! Inline-function recipe: replace a single-use function's call site with its body.
2//!
3//! Steps:
4//! 1. Locate the function definition at the given position (line:col may point to
5//! the function name in the definition or in a call site)
6//! 2. Find all call sites of this function within the same file (name-match)
7//! 3. Verify the function is called exactly once (or `--force` to override)
8//! 4. Substitute arguments for parameters throughout the body
9//! 5. Replace the call expression with the inlined body
10//! 6. Remove the function definition
11//!
12//! Supported languages: JavaScript, TypeScript (function declarations,
13//! arrow-function const bindings). Python (`def`) and Rust (`fn`) are
14//! structurally similar but less tested.
15//!
16//! Conservative: aborts rather than generating broken code.
17//! - Multiple `return` statements → error
18//! - Non-trivial control flow → error
19//! - Grammar unavailable → error
20
21use std::path::Path;
22
23use normalize_languages::parsers::parse_with_grammar;
24use normalize_languages::support_for_path;
25
26use crate::{PlannedEdit, RefactoringContext, RefactoringPlan};
27
28// ── Public output type ────────────────────────────────────────────────
29
30/// Outcome details for a planned inline-function operation.
31pub struct InlineFunctionOutcome {
32 pub plan: RefactoringPlan,
33 pub function_name: String,
34 pub call_site_line: usize,
35}
36
37// ── Entry point ───────────────────────────────────────────────────────
38
39/// Build an inline-function plan without touching the filesystem.
40///
41/// `file_path` is the path of the file (absolute or relative; used for grammar
42/// detection and error messages). `content` is the file's current text.
43/// `line` and `col` are 1-based and point to the function name — either in the
44/// definition or at a call site. `force` overrides the single-use check.
45pub fn plan_inline_function(
46 _ctx: &RefactoringContext,
47 file_abs: &Path,
48 content: &str,
49 line: usize,
50 col: usize,
51 force: bool,
52) -> Result<InlineFunctionOutcome, String> {
53 // ── 1. Grammar ────────────────────────────────────────────────────
54 let support = support_for_path(file_abs).ok_or_else(|| {
55 let ext = file_abs
56 .extension()
57 .and_then(|e| e.to_str())
58 .unwrap_or("<unknown>");
59 format!("inline-function: no language support for .{ext} files")
60 })?;
61 let grammar = support.grammar_name();
62 let tree = parse_with_grammar(grammar, content).ok_or_else(|| {
63 format!(
64 "inline-function: grammar for {grammar} not loaded — run `normalize grammars install`"
65 )
66 })?;
67
68 // ── 2. Resolve cursor position → function name ────────────────────
69 let cursor_byte = line_col_to_byte(content, line, col)?;
70 let root = tree.root_node();
71
72 // Walk from the deepest node at the cursor position upward, looking for
73 // either a function definition or a call_expression / call site.
74 let function_name =
75 resolve_function_name_at(&root, content, cursor_byte, grammar).ok_or_else(|| {
76 format!("inline-function: no function definition or call found at {line}:{col}")
77 })?;
78
79 // ── 3. Find the function definition ──────────────────────────────
80 let def = find_function_def(&root, content, &function_name, grammar).ok_or_else(|| {
81 format!("inline-function: definition of '{function_name}' not found in this file")
82 })?;
83
84 // ── 4. Validate the function body ─────────────────────────────────
85 let body_text = extract_body_text(content, &def)?;
86
87 // ── 5. Find call sites ───────────────────────────────────────────
88 let call_sites = find_call_sites(&root, content, &function_name, grammar);
89
90 match call_sites.len() {
91 0 => {
92 return Err(format!(
93 "inline-function: '{function_name}' has no call sites in this file"
94 ));
95 }
96 1 => {} // exactly one — proceed
97 _ if force => {} // multiple but --force
98 n => {
99 return Err(format!(
100 "inline-function: '{function_name}' is called {n} times; use --force to inline anyway (or inline the specific call manually)"
101 ));
102 }
103 }
104
105 let call_site = &call_sites[0];
106
107 // ── 6. Perform substitution ───────────────────────────────────────
108 let inlined = substitute_call(content, &def, call_site, &body_text)?;
109 let call_site_line = call_site.line;
110
111 // ── 7. Remove the function definition ────────────────────────────
112 // `inlined` already has the call replaced; now remove the def.
113 let final_content = remove_function_def(&inlined, &def, content)?;
114
115 let plan = RefactoringPlan {
116 operation: "inline-function".to_string(),
117 edits: vec![PlannedEdit {
118 file: file_abs.to_path_buf(),
119 original: content.to_string(),
120 new_content: final_content,
121 description: format!("inline {function_name}"),
122 }],
123 warnings: vec![],
124 };
125
126 Ok(InlineFunctionOutcome {
127 plan,
128 function_name,
129 call_site_line,
130 })
131}
132
133// ── Internal types ────────────────────────────────────────────────────
134
135/// A located function definition, with extracted parameter names and body span.
136struct FunctionDef {
137 /// The function name.
138 name: String,
139 /// Parameter names (positional, in order).
140 params: Vec<String>,
141 /// Byte range of the entire definition node (including any leading whitespace
142 /// up to the prior newline, for clean deletion).
143 def_start_byte: usize,
144 def_end_byte: usize,
145 /// Byte range of the function body (the `{ ... }` block, excluding braces).
146 body_start_byte: usize,
147 body_end_byte: usize,
148}
149
150/// A located call expression.
151struct CallSite {
152 /// Argument texts as they appear in source.
153 args: Vec<String>,
154 /// Byte range of the full call expression (e.g. `f(a, b)`).
155 call_start_byte: usize,
156 call_end_byte: usize,
157 /// 1-based line number of the call.
158 line: usize,
159}
160
161// ── Byte-position helpers ─────────────────────────────────────────────
162
163fn line_col_to_byte(content: &str, line: usize, col: usize) -> Result<usize, String> {
164 if line == 0 {
165 return Err("inline-function: line is 1-based; 0 is invalid".to_string());
166 }
167 let mut current_line = 1usize;
168 let mut line_start = 0usize;
169 for (i, ch) in content.char_indices() {
170 if current_line == line {
171 line_start = i;
172 break;
173 }
174 if ch == '\n' {
175 current_line += 1;
176 }
177 if current_line > line {
178 // Past end of file
179 return Err(format!(
180 "inline-function: line {line} is beyond end of file ({current_line} lines)"
181 ));
182 }
183 }
184 // If we reached the end without finding the line (file has exactly `line` lines
185 // with no trailing newline, so the loop exits without setting line_start for the
186 // last line):
187 if current_line < line {
188 // Check if content ends exactly at that line
189 return Err(format!(
190 "inline-function: line {line} is beyond end of file"
191 ));
192 }
193 let col_offset = col.saturating_sub(1); // 1-based → 0-based
194 let byte = line_start
195 + col_offset.min(
196 content[line_start..]
197 .find('\n')
198 .unwrap_or(content[line_start..].len()),
199 );
200 Ok(byte.min(content.len()))
201}
202
203fn byte_to_line(content: &str, byte: usize) -> usize {
204 content[..byte.min(content.len())]
205 .chars()
206 .filter(|&c| c == '\n')
207 .count()
208 + 1
209}
210
211// ── Grammar-aware traversal helpers ──────────────────────────────────
212
213/// Given a node at the cursor position, find the identifier name that is either
214/// a function name in a definition or a callee name in a call expression.
215fn resolve_function_name_at<'a>(
216 root: &tree_sitter::Node<'a>,
217 content: &str,
218 cursor_byte: usize,
219 _grammar: &str,
220) -> Option<String> {
221 let node = root.descendant_for_byte_range(cursor_byte, cursor_byte + 1)?;
222
223 // Walk up trying to identify a function definition or call expression.
224 let mut n = node;
225 loop {
226 let kind = n.kind();
227
228 // ── Function definitions ─────────────────────────────────────
229 // JS/TS: function_declaration, method_definition, arrow function via
230 // lexical_declaration (const f = (...) => ...)
231 // Python: function_definition
232 // Rust: function_item
233 if is_function_def_kind(kind) {
234 // Extract the name child
235 if let Some(name_node) = find_name_child(&n, content) {
236 return Some(name_node);
237 }
238 }
239
240 // ── Arrow / const function bindings ──────────────────────────
241 // JS/TS: `const f = (...) => ...` or `const f = function(...) {...}`
242 if (kind == "lexical_declaration" || kind == "variable_declaration")
243 && let Some(name) = extract_arrow_def_name(&n, content)
244 {
245 return Some(name);
246 }
247
248 // ── Call expressions ─────────────────────────────────────────
249 // JS/TS/Python/Rust: call_expression
250 if (kind == "call_expression" || kind == "call")
251 && let Some(callee) = n
252 .child_by_field_name("function")
253 .or_else(|| n.child_by_field_name("callee"))
254 {
255 let callee_text = &content[callee.start_byte()..callee.end_byte()];
256 // Only simple identifier calls (not method calls like a.b())
257 if !callee_text.contains('.') && !callee_text.contains(':') {
258 return Some(callee_text.to_string());
259 }
260 }
261
262 match n.parent() {
263 Some(p) if p.id() != root.id() => n = p,
264 _ => break,
265 }
266 }
267 None
268}
269
270fn is_function_def_kind(kind: &str) -> bool {
271 matches!(
272 kind,
273 "function_declaration"
274 | "function_definition" // Python
275 | "function_item" // Rust
276 | "method_definition"
277 | "generator_function_declaration"
278 )
279}
280
281fn is_arrow_or_func_expr_kind(kind: &str) -> bool {
282 matches!(
283 kind,
284 "arrow_function" | "function_expression" | "generator_function"
285 )
286}
287
288/// Find a parameter-list child node by kind heuristic (fallback when field names aren't available).
289fn find_params_child<'a>(node: &tree_sitter::Node<'a>) -> Option<tree_sitter::Node<'a>> {
290 let mut c = node.walk();
291 let mut found = None;
292 if c.goto_first_child() {
293 loop {
294 let n = c.node();
295 if matches!(
296 n.kind(),
297 "formal_parameters" | "parameters" | "parameter_list"
298 ) {
299 found = Some(n);
300 break;
301 }
302 if !c.goto_next_sibling() {
303 break;
304 }
305 }
306 }
307 found
308}
309
310/// Find a body/block child node by kind heuristic.
311/// For `{ ... }` bodies: find `statement_block` / `block`.
312/// For arrow expression bodies: find the node after `=>`.
313fn find_body_child<'a>(node: &tree_sitter::Node<'a>) -> Option<tree_sitter::Node<'a>> {
314 // First pass: look for explicit block kinds.
315 let mut c = node.walk();
316 if c.goto_first_child() {
317 loop {
318 let n = c.node();
319 if matches!(n.kind(), "statement_block" | "block" | "function_body") {
320 return Some(n);
321 }
322 if !c.goto_next_sibling() {
323 break;
324 }
325 }
326 }
327 // Second pass: for arrow expressions, find the node after `=>`.
328 let mut c = node.walk();
329 let mut past_arrow = false;
330 if c.goto_first_child() {
331 loop {
332 let n = c.node();
333 if past_arrow && n.is_named() {
334 return Some(n);
335 }
336 if n.kind() == "=>" {
337 past_arrow = true;
338 }
339 if !c.goto_next_sibling() {
340 break;
341 }
342 }
343 }
344 None
345}
346
347/// Extract the function name from a definition node.
348fn find_name_child(node: &tree_sitter::Node<'_>, content: &str) -> Option<String> {
349 // Try the `name` field first (JS/TS function_declaration, Python function_definition)
350 if let Some(name_node) = node.child_by_field_name("name") {
351 return Some(content[name_node.start_byte()..name_node.end_byte()].to_string());
352 }
353 // Rust function_item uses `name`
354 None
355}
356
357/// If a `lexical_declaration` or `variable_declaration` node binds a function
358/// (arrow or function expression), return the variable name.
359fn extract_arrow_def_name(node: &tree_sitter::Node<'_>, content: &str) -> Option<String> {
360 let mut c = node.walk();
361 if c.goto_first_child() {
362 loop {
363 let child = c.node();
364 if child.kind() == "variable_declarator"
365 && let Some(name) = arrow_declarator_name(&child, content)
366 {
367 return Some(name);
368 }
369 if !c.goto_next_sibling() {
370 break;
371 }
372 }
373 }
374 None
375}
376
377/// Extract the name from a `variable_declarator` if its value is an arrow/function expression.
378/// Handles both field-based grammars and child-order-based grammars.
379fn arrow_declarator_name(decl: &tree_sitter::Node<'_>, content: &str) -> Option<String> {
380 // Try named field "name" + "value" first.
381 let name_via_field = decl.child_by_field_name("name").or_else(|| {
382 // Fallback: first named child that is an identifier.
383 let mut c = decl.walk();
384 let mut found = None;
385 if c.goto_first_child() {
386 loop {
387 let n = c.node();
388 if n.kind() == "identifier" {
389 found = Some(n);
390 break;
391 }
392 if !c.goto_next_sibling() {
393 break;
394 }
395 }
396 }
397 found
398 });
399 let name_text = name_via_field.map(|n| content[n.start_byte()..n.end_byte()].to_string())?;
400
401 // Check if the value is an arrow/function expression, using either fields or children.
402 let has_func_value = decl
403 .child_by_field_name("value")
404 .map(|v| is_arrow_or_func_expr_kind(v.kind()))
405 .unwrap_or_else(|| {
406 // Fallback: scan children for an arrow/function expression.
407 let mut c = decl.walk();
408 let mut found = false;
409 if c.goto_first_child() {
410 loop {
411 let n = c.node();
412 if is_arrow_or_func_expr_kind(n.kind()) {
413 found = true;
414 break;
415 }
416 if !c.goto_next_sibling() {
417 break;
418 }
419 }
420 }
421 found
422 });
423
424 if has_func_value {
425 Some(name_text)
426 } else {
427 None
428 }
429}
430
431// ── Function definition finder ────────────────────────────────────────
432
433fn find_function_def(
434 root: &tree_sitter::Node<'_>,
435 content: &str,
436 name: &str,
437 _grammar: &str,
438) -> Option<FunctionDef> {
439 // Walk all nodes in the tree looking for function definitions named `name`.
440 let mut cursor = root.walk();
441 find_function_def_recursive(&mut cursor, *root, content, name)
442}
443
444fn find_function_def_recursive(
445 cursor: &mut tree_sitter::TreeCursor<'_>,
446 node: tree_sitter::Node<'_>,
447 content: &str,
448 name: &str,
449) -> Option<FunctionDef> {
450 let kind = node.kind();
451
452 // Check if this node is a function definition with the right name.
453 if is_function_def_kind(kind)
454 && let Some(found_name) = find_name_child(&node, content)
455 && found_name == name
456 {
457 return extract_function_def(&node, content, name, true);
458 }
459
460 // JS/TS: `const f = (...) => ...` — lexical_declaration containing an arrow_function
461 if (kind == "lexical_declaration" || kind == "variable_declaration")
462 && let Some(def) = try_extract_arrow_def(&node, content, name)
463 {
464 return Some(def);
465 }
466
467 // Recurse into children.
468 if cursor.goto_first_child() {
469 loop {
470 let child = cursor.node();
471 if let Some(result) = find_function_def_recursive(cursor, child, content, name) {
472 // Restore before returning so callers don't observe cursor side-effects.
473 // (tree-sitter cursors are position-stateful but we own this cursor.)
474 return Some(result);
475 }
476 if !cursor.goto_next_sibling() {
477 break;
478 }
479 }
480 cursor.goto_parent();
481 }
482
483 None
484}
485
486/// Try to extract an arrow-function definition from a `const f = (...) => ...` declaration.
487fn try_extract_arrow_def(
488 decl_node: &tree_sitter::Node<'_>,
489 content: &str,
490 name: &str,
491) -> Option<FunctionDef> {
492 // Look for a variable_declarator child with the right name binding a function.
493 let mut decl_cursor = decl_node.walk();
494 if decl_cursor.goto_first_child() {
495 loop {
496 let child = decl_cursor.node();
497 if child.kind() == "variable_declarator"
498 && arrow_declarator_name(&child, content).as_deref() == Some(name)
499 {
500 return extract_function_def(decl_node, content, name, true);
501 }
502 if !decl_cursor.goto_next_sibling() {
503 break;
504 }
505 }
506 }
507 None
508}
509
510/// Extract structured information from a function definition node.
511fn extract_function_def(
512 node: &tree_sitter::Node<'_>,
513 content: &str,
514 name: &str,
515 _is_statement: bool,
516) -> Option<FunctionDef> {
517 // For arrow functions inside `const f = ...`, we need the body from the
518 // inner arrow_function node, but the span from the outer statement.
519 let (param_node, body_node) =
520 if node.kind() == "lexical_declaration" || node.kind() == "variable_declaration" {
521 // Find the variable_declarator → value (arrow_function or function_expression)
522 let mut c = node.walk();
523 let mut found_decl: Option<tree_sitter::Node<'_>> = None;
524 if c.goto_first_child() {
525 loop {
526 let child = c.node();
527 if child.kind() == "variable_declarator" {
528 let vname = child
529 .child_by_field_name("name")
530 .map(|n| &content[n.start_byte()..n.end_byte()]);
531 if vname == Some(name) {
532 found_decl = Some(child);
533 break;
534 }
535 }
536 if !c.goto_next_sibling() {
537 break;
538 }
539 }
540 }
541 let decl = found_decl?;
542 // The function value may be accessed via a named "value" field or as a child.
543 let value = decl.child_by_field_name("value").or_else(|| {
544 // Fallback: find the first arrow/function expression child.
545 let mut cc = decl.walk();
546 let mut found = None;
547 if cc.goto_first_child() {
548 loop {
549 let n = cc.node();
550 if is_arrow_or_func_expr_kind(n.kind()) {
551 found = Some(n);
552 break;
553 }
554 if !cc.goto_next_sibling() {
555 break;
556 }
557 }
558 }
559 found
560 })?;
561 let params = value
562 .child_by_field_name("parameters")
563 .or_else(|| value.child_by_field_name("formal_parameters"))
564 .or_else(|| find_params_child(&value))?;
565 let body = value
566 .child_by_field_name("body")
567 .or_else(|| find_body_child(&value))?;
568 (params, body)
569 } else {
570 // function_declaration / function_definition / function_item
571 let params = node
572 .child_by_field_name("parameters")
573 .or_else(|| node.child_by_field_name("formal_parameters"))
574 .or_else(|| find_params_child(node))?;
575 let body = node
576 .child_by_field_name("body")
577 .or_else(|| find_body_child(node))?;
578 (params, body)
579 };
580
581 let params = extract_parameter_names(¶m_node, content);
582
583 // Determine body start/end (inside the braces, if present).
584 // For arrow functions with expression bodies (no braces), treat the whole
585 // value as the body.
586 let (body_start_byte, body_end_byte) =
587 if body_node.kind() == "statement_block" || body_node.kind() == "block" {
588 // Skip the opening `{` and closing `}`
589 let inner_start = body_node.start_byte() + 1;
590 let inner_end = body_node.end_byte() - 1;
591 (inner_start, inner_end)
592 } else {
593 // Expression body (arrow function without braces): `(x) => x + 1`
594 // The entire body is the expression.
595 (body_node.start_byte(), body_node.end_byte())
596 };
597
598 // Snap def start to line start for clean deletion.
599 let def_start_byte = {
600 let raw = node.start_byte();
601 content[..raw].rfind('\n').map(|i| i + 1).unwrap_or(0)
602 };
603 let def_end_byte = {
604 let raw = node.end_byte();
605 // Include the trailing newline if present.
606 if raw < content.len() && content.as_bytes()[raw] == b'\n' {
607 raw + 1
608 } else {
609 raw
610 }
611 };
612
613 Some(FunctionDef {
614 name: name.to_string(),
615 params,
616 def_start_byte,
617 def_end_byte,
618 body_start_byte,
619 body_end_byte,
620 })
621}
622
623/// Extract positional parameter names from a parameter list node.
624fn extract_parameter_names(params_node: &tree_sitter::Node<'_>, content: &str) -> Vec<String> {
625 let mut names = vec![];
626 let mut c = params_node.walk();
627 if c.goto_first_child() {
628 loop {
629 let child = c.node();
630 let kind = child.kind();
631 // JS/TS: identifier, required_parameter, optional_parameter, rest_parameter,
632 // assignment_pattern (default param)
633 // Python: identifier, typed_parameter, default_parameter
634 // Rust: pattern (identifier, typed)
635 let param_name = match kind {
636 "identifier" => Some(&content[child.start_byte()..child.end_byte()]),
637 "required_parameter" | "optional_parameter" => child
638 .child_by_field_name("pattern")
639 .or_else(|| {
640 // Fall back to first named child that is an identifier
641 let mut cc = child.walk();
642 if cc.goto_first_child() {
643 loop {
644 let n = cc.node();
645 if n.kind() == "identifier" {
646 return Some(n);
647 }
648 if !cc.goto_next_sibling() {
649 break;
650 }
651 }
652 }
653 None
654 })
655 .map(|n| &content[n.start_byte()..n.end_byte()]),
656 "typed_parameter" | "default_parameter" => {
657 // Python: first child is usually the name identifier
658 let mut cc = child.walk();
659 let mut found = None;
660 if cc.goto_first_child() {
661 loop {
662 let n = cc.node();
663 if n.kind() == "identifier" {
664 found = Some(&content[n.start_byte()..n.end_byte()]);
665 break;
666 }
667 if !cc.goto_next_sibling() {
668 break;
669 }
670 }
671 }
672 found
673 }
674 // Rust: parameter has `pattern` and `type` fields
675 "parameter" => child
676 .child_by_field_name("pattern")
677 .map(|n| &content[n.start_byte()..n.end_byte()]),
678 _ => None,
679 };
680 if let Some(n) = param_name
681 && !n.is_empty()
682 {
683 names.push(n.to_string());
684 }
685 if !c.goto_next_sibling() {
686 break;
687 }
688 }
689 }
690 names
691}
692
693// ── Body text extraction and validation ───────────────────────────────
694
695/// Extract the body text from a function definition, stripping surrounding braces
696/// and normalizing indentation. Returns an error if the body is too complex to
697/// inline safely (e.g. multiple `return` statements).
698fn extract_body_text(content: &str, def: &FunctionDef) -> Result<String, String> {
699 let raw_body = &content[def.body_start_byte..def.body_end_byte];
700
701 // Count `return` statements. A conservative text search is good enough for
702 // a first-pass safety check — if you have `return` in a string or comment,
703 // this will over-count and refuse rather than under-count and generate broken code.
704 // That's the correct conservative behavior.
705 let return_count = count_return_statements(raw_body);
706 if return_count > 1 {
707 return Err(format!(
708 "inline-function: '{}' has {} return statements; inlining would require control-flow analysis — aborting (too complex)",
709 def.name, return_count
710 ));
711 }
712
713 Ok(raw_body.to_string())
714}
715
716/// Count `return` keyword occurrences at word boundaries in `text`.
717fn count_return_statements(text: &str) -> usize {
718 let mut count = 0usize;
719 let mut i = 0usize;
720 let bytes = text.as_bytes();
721 while i + 6 <= bytes.len() {
722 if &bytes[i..i + 6] == b"return" {
723 // Check word boundaries.
724 let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
725 let after = bytes.get(i + 6).copied();
726 let after_ok = after.is_none_or(|b| !b.is_ascii_alphanumeric() && b != b'_');
727 if before_ok && after_ok {
728 count += 1;
729 }
730 }
731 i += 1;
732 }
733 count
734}
735
736// ── Call site finder ──────────────────────────────────────────────────
737
738fn find_call_sites(
739 root: &tree_sitter::Node<'_>,
740 content: &str,
741 name: &str,
742 _grammar: &str,
743) -> Vec<CallSite> {
744 let mut sites = vec![];
745 let mut cursor = root.walk();
746 find_call_sites_recursive(&mut cursor, *root, content, name, &mut sites);
747 sites
748}
749
750fn find_call_sites_recursive(
751 cursor: &mut tree_sitter::TreeCursor<'_>,
752 node: tree_sitter::Node<'_>,
753 content: &str,
754 name: &str,
755 sites: &mut Vec<CallSite>,
756) {
757 let kind = node.kind();
758
759 if kind == "call_expression" || kind == "call" {
760 // Check callee is our function name (simple identifier, not a.b or a::b).
761 let callee = node
762 .child_by_field_name("function")
763 .or_else(|| node.child_by_field_name("callee"));
764 if let Some(callee_node) = callee {
765 let callee_text = &content[callee_node.start_byte()..callee_node.end_byte()];
766 if callee_text == name {
767 // Extract arguments.
768 let args = extract_call_args(&node, content);
769 let line = byte_to_line(content, node.start_byte());
770
771 sites.push(CallSite {
772 args,
773 call_start_byte: node.start_byte(),
774 call_end_byte: node.end_byte(),
775 line,
776 });
777 }
778 }
779 }
780
781 if cursor.goto_first_child() {
782 loop {
783 let child = cursor.node();
784 find_call_sites_recursive(cursor, child, content, name, sites);
785 if !cursor.goto_next_sibling() {
786 break;
787 }
788 }
789 cursor.goto_parent();
790 }
791}
792
793/// Extract argument texts from a call_expression node.
794fn extract_call_args(call_node: &tree_sitter::Node<'_>, content: &str) -> Vec<String> {
795 let mut args = vec![];
796 let args_node = call_node.child_by_field_name("arguments").or_else(|| {
797 // Fallback: look for argument_list child (Python)
798 let mut c = call_node.walk();
799 let mut found = None;
800 if c.goto_first_child() {
801 loop {
802 let n = c.node();
803 if matches!(n.kind(), "argument_list" | "arguments") {
804 found = Some(n);
805 break;
806 }
807 if !c.goto_next_sibling() {
808 break;
809 }
810 }
811 }
812 found
813 });
814
815 let Some(args_node) = args_node else {
816 return args;
817 };
818
819 let mut c = args_node.walk();
820 if c.goto_first_child() {
821 loop {
822 let child = c.node();
823 let kind = child.kind();
824 // Skip punctuation nodes (`,`, `(`, `)`)
825 if kind != "," && kind != "(" && kind != ")" && child.is_named() {
826 args.push(content[child.start_byte()..child.end_byte()].to_string());
827 }
828 if !c.goto_next_sibling() {
829 break;
830 }
831 }
832 }
833 args
834}
835
836// ── Substitution ──────────────────────────────────────────────────────
837
838/// Replace the call site with the inlined function body.
839///
840/// Steps:
841/// 1. Strip the body of leading/trailing whitespace
842/// 2. Replace each parameter name with the corresponding argument
843/// 3. Strip the `return` keyword (if present) when the call is in expression position
844/// 4. Replace the call span in `content`
845fn substitute_call(
846 content: &str,
847 def: &FunctionDef,
848 call: &CallSite,
849 body_text: &str,
850) -> Result<String, String> {
851 // Check argument count matches parameter count.
852 if call.args.len() != def.params.len() {
853 return Err(format!(
854 "inline-function: '{}' expects {} arguments but call site provides {} — aborting",
855 def.name,
856 def.params.len(),
857 call.args.len()
858 ));
859 }
860
861 // ── Trim the body ─────────────────────────────────────────────────
862 let trimmed = body_text.trim();
863
864 // ── Strip `return` if present ─────────────────────────────────────
865 let stripped = strip_single_return(trimmed);
866
867 // ── Substitute parameters → arguments ────────────────────────────
868 let mut result = stripped.to_string();
869 for (param, arg) in def.params.iter().zip(call.args.iter()) {
870 result = normalize_edit::replace_all_words(&result, param, arg);
871 }
872
873 // ── Determine what replaces the call site in `content` ────────────
874 //
875 // If the call is the sole expression in an expression_statement, we need to
876 // handle whether to keep the semicolon / statement boundary. In the simple
877 // case we just replace the call expression bytes with the inlined body.
878 //
879 // If the function body contained a statement block, we need to decide whether
880 // to emit the block inline or unwrap it. For now: if the call is a statement
881 // *and* the body looks like a block (contains `;`), keep it as a block.
882 // Otherwise, unwrap to an expression.
883 let replacement = result.trim().to_string();
884
885 // Build new content: replace the call bytes with the replacement.
886 let mut new_content = String::new();
887 new_content.push_str(&content[..call.call_start_byte]);
888 new_content.push_str(&replacement);
889 new_content.push_str(&content[call.call_end_byte..]);
890
891 Ok(new_content)
892}
893
894/// Strip a leading `return ` from an expression if present (single `return`).
895fn strip_single_return(s: &str) -> &str {
896 let s = s.trim_start();
897 if let Some(rest) = s.strip_prefix("return") {
898 // Must be followed by whitespace or end-of-string.
899 let after = rest.trim_start_matches([' ', '\t']);
900 // Strip trailing semicolon if the body was just `return expr;`
901 after.strip_suffix(';').unwrap_or(after).trim()
902 } else {
903 s
904 }
905}
906
907// ── Definition removal ────────────────────────────────────────────────
908
909/// Remove the function definition from `inlined` (which already has the call replaced).
910///
911/// Because the edit we already applied (call replacement) may have shifted byte offsets,
912/// we re-locate the function definition by name in `inlined` using a text-based approach.
913///
914/// `original` is the original file content (used to know the original def span).
915fn remove_function_def(inlined: &str, def: &FunctionDef, original: &str) -> Result<String, String> {
916 // The byte delta introduced by the call replacement.
917 // original length - (call_site length) + replacement length
918 // We don't have the call site bytes here, but we can use a tree-sitter re-parse
919 // of `inlined` to re-find the definition.
920 //
921 // For simplicity: use a grammar-agnostic approach — find the def by locating
922 // the whole-word name on the def's original line in the new content, then
923 // use the Editor's find_symbol. But since we don't have a path here, we use
924 // the editor's text-only utility.
925 //
926 // Actually, the cleanest approach: re-parse `inlined` to find the def.
927 // Since we don't have the path or grammar here (they're not in FunctionDef),
928 // we use the original line number adjusted for any inserted/removed content
929 // above the definition.
930 //
931 // However, the simplest correct approach is: the call site and the definition
932 // are in different parts of the file. If the call site is AFTER the definition,
933 // the definition bytes haven't moved; we can delete them directly from `inlined`.
934 // If the call site is BEFORE the definition, the definition bytes have shifted.
935 //
936 // We know def_start_byte from the original content.
937 // The call replaced `call_start_byte..call_end_byte` with `replacement`.
938 // The shift in bytes = replacement.len() - (call_end_byte - call_start_byte).
939 //
940 // But we don't have call_start/end here. Let's take the safe path:
941 // diff the original and inlined to compute the shift, then adjust.
942 //
943 // Simplest path: find the function definition text in `inlined` by searching
944 // for the original definition text (which is unchanged since the call was elsewhere).
945
946 let orig_def_text = &original[def.def_start_byte..def.def_end_byte];
947
948 // Find the definition in `inlined` by exact text match.
949 if let Some(pos) = inlined.find(orig_def_text) {
950 let mut result = String::new();
951 result.push_str(&inlined[..pos]);
952 result.push_str(&inlined[pos + orig_def_text.len()..]);
953 // Clean up any double-blank lines introduced by the deletion.
954 Ok(collapse_triple_newlines(result))
955 } else {
956 // Fallback: try to delete by adjusted byte offset.
957 // Compute byte shift: compare lengths before/after the definition.
958 // This handles the case where the call was in the def text itself (recursive),
959 // which is already blocked by our single-use check — but be safe.
960 Err(format!(
961 "inline-function: could not locate definition of '{}' in modified content — aborting",
962 def.name
963 ))
964 }
965}
966
967/// Collapse three or more consecutive newlines into two (one blank line).
968fn collapse_triple_newlines(s: String) -> String {
969 let mut result = s;
970 loop {
971 let before = result.len();
972 result = result.replace("\n\n\n", "\n\n");
973 if result.len() == before {
974 break;
975 }
976 }
977 result
978}