1use crate::project::ProjectRoot;
2use crate::rename::{RenameEdit, apply_edits, find_all_word_matches};
3use crate::symbols::{find_symbol, find_symbol_range};
4use anyhow::{Result, bail};
5use serde::{Deserialize, Serialize};
6use std::fs;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ParamSpec {
10 pub name: String,
11 #[serde(rename = "type", default)]
12 pub param_type: Option<String>,
13 #[serde(default)]
14 pub default: Option<String>,
15}
16
17#[derive(Debug, Clone, Serialize)]
18pub struct ChangeSignatureResult {
19 pub success: bool,
20 pub message: String,
21 pub old_params: Vec<String>,
22 pub new_params: Vec<String>,
23 pub call_sites_updated: usize,
24 pub modified_files: Vec<String>,
25 pub edits: Vec<RenameEdit>,
26}
27
28pub fn change_signature(
34 project: &ProjectRoot,
35 file_path: &str,
36 function_name: &str,
37 name_path: Option<&str>,
38 new_params: &[ParamSpec],
39 dry_run: bool,
40) -> Result<ChangeSignatureResult> {
41 let symbols = find_symbol(project, function_name, Some(file_path), true, true, 1)?;
43 let _sym = symbols.first().ok_or_else(|| {
44 anyhow::anyhow!("Function '{}' not found in '{}'", function_name, file_path)
45 })?;
46
47 let resolved = project.resolve(file_path)?;
48 let source = fs::read_to_string(&resolved)?;
49 let (start_byte, end_byte) = find_symbol_range(project, file_path, function_name, name_path)?;
50 let full_def = &source[start_byte..end_byte];
51
52 let paren_start = full_def
54 .find('(')
55 .ok_or_else(|| anyhow::anyhow!("No parameter list found in function definition"))?;
56 let paren_end = find_matching_paren(full_def, paren_start)?;
57 let old_params_str = &full_def[paren_start + 1..paren_end];
58
59 let ext = std::path::Path::new(file_path)
60 .extension()
61 .and_then(|e| e.to_str())
62 .unwrap_or("");
63
64 let old_param_names = parse_param_names(old_params_str, ext);
65 let old_mappable: Vec<&str> = old_param_names
67 .iter()
68 .filter(|p| !is_self_param(p))
69 .map(|s| s.as_str())
70 .collect();
71
72 let new_param_string = build_new_param_string(new_params, ext, old_params_str);
74
75 let abs_paren_start = start_byte + paren_start;
77 let _abs_paren_end = start_byte + paren_end;
78
79 let def_line = source[..abs_paren_start + 1].lines().count();
81 let def_line_start = source[..abs_paren_start + 1]
82 .rfind('\n')
83 .map(|p| p + 1)
84 .unwrap_or(0);
85 let def_col = abs_paren_start + 1 - def_line_start + 1;
86
87 let old_params_text = old_params_str.to_string();
88
89 let mut edits = vec![RenameEdit {
90 file_path: file_path.to_string(),
91 line: def_line,
92 column: def_col,
93 old_text: old_params_text.clone(),
94 new_text: new_param_string.clone(),
95 }];
96
97 let param_mapping = build_param_mapping(&old_mappable, new_params);
99
100 let matches = find_all_word_matches(project, function_name)?;
102 let sym_line = _sym.line;
103
104 let mut call_sites_updated = 0;
105
106 for (ref_file, line, col) in &matches {
107 if ref_file == file_path && *line == sym_line {
109 continue;
110 }
111
112 let ref_resolved = match project.resolve(ref_file) {
113 Ok(p) => p,
114 Err(_) => continue,
115 };
116 let ref_content = match fs::read_to_string(&ref_resolved) {
117 Ok(c) => c,
118 Err(_) => continue,
119 };
120 let ref_lines: Vec<&str> = ref_content.lines().collect();
121 if *line == 0 || *line > ref_lines.len() {
122 continue;
123 }
124 let line_text = ref_lines[*line - 1];
125
126 let name_end = *col - 1 + function_name.len();
128 if name_end >= line_text.len() {
129 continue;
130 }
131 let after = line_text[name_end..].trim_start();
132 if !after.starts_with('(') {
133 continue;
134 }
135
136 let call_rest = &line_text[*col - 1..];
138 let call_paren = match call_rest.find('(') {
139 Some(p) => p,
140 None => continue,
141 };
142 let call_paren_end = match find_matching_paren(call_rest, call_paren) {
143 Ok(p) => p,
144 Err(_) => continue,
145 };
146 let args_str = &call_rest[call_paren + 1..call_paren_end];
147 let old_args = split_args(args_str);
148
149 let new_args = build_new_args(&old_args, ¶m_mapping, new_params);
151 let new_args_str = new_args.join(", ");
152
153 if args_str.trim() != new_args_str.trim() {
154 let args_col = *col + call_paren + 1;
155 edits.push(RenameEdit {
156 file_path: ref_file.clone(),
157 line: *line,
158 column: args_col,
159 old_text: args_str.to_string(),
160 new_text: new_args_str,
161 });
162 call_sites_updated += 1;
163 }
164 }
165
166 let mut modified_files: Vec<String> = edits.iter().map(|e| e.file_path.clone()).collect();
167 modified_files.sort();
168 modified_files.dedup();
169
170 let result = ChangeSignatureResult {
171 success: true,
172 message: format!(
173 "Changed signature of '{}': {} params → {}, updated {} call site(s)",
174 function_name,
175 old_mappable.len(),
176 new_params.len(),
177 call_sites_updated
178 ),
179 old_params: old_mappable.iter().map(|s| s.to_string()).collect(),
180 new_params: new_params.iter().map(|p| p.name.clone()).collect(),
181 call_sites_updated,
182 modified_files,
183 edits: edits.clone(),
184 };
185
186 if !dry_run {
187 apply_edits(project, &edits)?;
188 }
189
190 Ok(result)
191}
192
193fn parse_param_names(params_str: &str, ext: &str) -> Vec<String> {
195 if params_str.trim().is_empty() {
196 return vec![];
197 }
198 params_str
199 .split(',')
200 .map(|p| {
201 let p = p.trim();
202 let p = p.split('=').next().unwrap_or(p).trim();
204 match ext {
205 "rs" => p.split(':').next().unwrap_or(p).trim().to_string(),
206 "go" => p.split_whitespace().next().unwrap_or(p).to_string(),
207 "py" => {
208 if p.contains(':') {
209 p.split(':').next().unwrap_or(p).trim().to_string()
210 } else {
211 p.to_string()
212 }
213 }
214 _ => {
215 if p.contains(':') {
216 p.split(':').next().unwrap_or(p).trim().to_string()
217 } else {
218 p.split_whitespace().last().unwrap_or(p).to_string()
219 }
220 }
221 }
222 })
223 .collect()
224}
225
226fn is_self_param(name: &str) -> bool {
227 matches!(name, "self" | "&self" | "&mut self" | "this")
228}
229
230fn build_param_mapping(old_params: &[&str], new_params: &[ParamSpec]) -> Vec<Option<usize>> {
232 new_params
233 .iter()
234 .map(|np| old_params.iter().position(|&op| op == np.name))
235 .collect()
236}
237
238fn build_new_param_string(new_params: &[ParamSpec], ext: &str, old_params_str: &str) -> String {
240 let old_parts: Vec<&str> = old_params_str.split(',').map(|p| p.trim()).collect();
242 let has_self = old_parts
243 .first()
244 .is_some_and(|p| is_self_param(p.split(':').next().unwrap_or(p).trim()));
245
246 let mut parts = Vec::new();
247 if has_self {
248 parts.push(old_parts[0].to_string());
249 }
250
251 for param in new_params {
252 let part = match ext {
253 "rs" => {
254 if let Some(t) = ¶m.param_type {
255 format!("{}: {}", param.name, t)
256 } else {
257 param.name.clone()
258 }
259 }
260 "py" => {
261 let mut s = param.name.clone();
262 if let Some(t) = ¶m.param_type {
263 s = format!("{}: {}", s, t);
264 }
265 if let Some(d) = ¶m.default {
266 s = format!("{} = {}", s, d);
267 }
268 s
269 }
270 "go" => {
271 if let Some(t) = ¶m.param_type {
272 format!("{} {}", param.name, t)
273 } else {
274 param.name.clone()
275 }
276 }
277 "ts" | "tsx" | "js" | "jsx" => {
278 let mut s = param.name.clone();
279 if let Some(t) = ¶m.param_type {
280 s = format!("{}: {}", s, t);
281 }
282 if let Some(d) = ¶m.default {
283 s = format!("{} = {}", s, d);
284 }
285 s
286 }
287 _ => {
288 if let Some(t) = ¶m.param_type {
289 format!("{} {}", t, param.name)
290 } else {
291 param.name.clone()
292 }
293 }
294 };
295 parts.push(part);
296 }
297
298 parts.join(", ")
299}
300
301fn build_new_args(
303 old_args: &[String],
304 mapping: &[Option<usize>],
305 new_params: &[ParamSpec],
306) -> Vec<String> {
307 mapping
308 .iter()
309 .zip(new_params.iter())
310 .map(|(old_idx, param)| {
311 if let Some(idx) = old_idx {
312 old_args.get(*idx).cloned().unwrap_or_else(|| {
314 param
315 .default
316 .clone()
317 .unwrap_or_else(|| format!("/* {} */", param.name))
318 })
319 } else {
320 param
322 .default
323 .clone()
324 .unwrap_or_else(|| format!("/* {} */", param.name))
325 }
326 })
327 .collect()
328}
329
330fn find_matching_paren(s: &str, open_pos: usize) -> Result<usize> {
331 let mut depth = 0;
332 for (i, ch) in s[open_pos..].char_indices() {
333 match ch {
334 '(' => depth += 1,
335 ')' => {
336 depth -= 1;
337 if depth == 0 {
338 return Ok(open_pos + i);
339 }
340 }
341 _ => {}
342 }
343 }
344 bail!("Unmatched parenthesis")
345}
346
347fn split_args(s: &str) -> Vec<String> {
348 if s.trim().is_empty() {
349 return vec![];
350 }
351 let mut args = Vec::new();
352 let mut depth = 0;
353 let mut current = String::new();
354 for ch in s.chars() {
355 match ch {
356 '(' | '[' | '{' => {
357 depth += 1;
358 current.push(ch);
359 }
360 ')' | ']' | '}' => {
361 depth -= 1;
362 current.push(ch);
363 }
364 ',' if depth == 0 => {
365 args.push(current.trim().to_string());
366 current.clear();
367 }
368 _ => current.push(ch),
369 }
370 }
371 if !current.trim().is_empty() {
372 args.push(current.trim().to_string());
373 }
374 args
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_parse_param_names_rust() {
383 let names = parse_param_names("a: i32, b: String, c: &str", "rs");
384 assert_eq!(names, vec!["a", "b", "c"]);
385 }
386
387 #[test]
388 fn test_parse_param_names_python() {
389 let names = parse_param_names("self, x, y: int, z=10", "py");
390 assert_eq!(names, vec!["self", "x", "y", "z"]);
391 }
392
393 #[test]
394 fn test_parse_param_names_go() {
395 let names = parse_param_names("x int, y string", "go");
396 assert_eq!(names, vec!["x", "y"]);
397 }
398
399 #[test]
400 fn test_build_param_mapping() {
401 let old = vec!["a", "b", "c"];
402 let new_params = vec![
403 ParamSpec {
404 name: "c".into(),
405 param_type: None,
406 default: None,
407 },
408 ParamSpec {
409 name: "a".into(),
410 param_type: None,
411 default: None,
412 },
413 ParamSpec {
414 name: "d".into(),
415 param_type: None,
416 default: Some("0".into()),
417 },
418 ];
419 let mapping = build_param_mapping(&old, &new_params);
420 assert_eq!(mapping, vec![Some(2), Some(0), None]);
421 }
422
423 #[test]
424 fn test_build_new_args() {
425 let old_args = vec!["1".into(), "2".into(), "3".into()];
426 let new_params = vec![
427 ParamSpec {
428 name: "c".into(),
429 param_type: None,
430 default: None,
431 },
432 ParamSpec {
433 name: "a".into(),
434 param_type: None,
435 default: None,
436 },
437 ParamSpec {
438 name: "d".into(),
439 param_type: None,
440 default: Some("0".into()),
441 },
442 ];
443 let mapping = vec![Some(2), Some(0), None];
444 let result = build_new_args(&old_args, &mapping, &new_params);
445 assert_eq!(result, vec!["3", "1", "0"]);
446 }
447
448 #[test]
449 fn test_build_new_param_string_rust() {
450 let params = vec![
451 ParamSpec {
452 name: "x".into(),
453 param_type: Some("i32".into()),
454 default: None,
455 },
456 ParamSpec {
457 name: "y".into(),
458 param_type: Some("i32".into()),
459 default: None,
460 },
461 ];
462 let result = build_new_param_string(¶ms, "rs", "a: i32");
463 assert_eq!(result, "x: i32, y: i32");
464 }
465
466 #[test]
467 fn test_build_new_param_string_preserves_self() {
468 let params = vec![ParamSpec {
469 name: "x".into(),
470 param_type: Some("i32".into()),
471 default: None,
472 }];
473 let result = build_new_param_string(¶ms, "rs", "&self, a: i32");
474 assert_eq!(result, "&self, x: i32");
475 }
476
477 #[test]
478 fn test_build_new_param_string_python() {
479 let params = vec![
480 ParamSpec {
481 name: "x".into(),
482 param_type: Some("int".into()),
483 default: None,
484 },
485 ParamSpec {
486 name: "y".into(),
487 param_type: None,
488 default: Some("0".into()),
489 },
490 ];
491 let result = build_new_param_string(¶ms, "py", "a, b");
492 assert_eq!(result, "x: int, y = 0");
493 }
494}