ide_assists/handlers/
remove_unused_param.rs

1use ide_db::{EditionedFileId, defs::Definition, search::FileReference};
2use syntax::{
3    AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, T, TextRange,
4    algo::{find_node_at_range, least_common_ancestor_element},
5    ast::{self, HasArgList},
6    syntax_editor::Element,
7};
8
9use SyntaxKind::WHITESPACE;
10
11use crate::{
12    AssistContext, AssistId, Assists, assist_context::SourceChangeBuilder, utils::next_prev,
13};
14
15// Assist: remove_unused_param
16//
17// Removes unused function parameter.
18//
19// ```
20// fn frobnicate(x: i32$0) {}
21//
22// fn main() {
23//     frobnicate(92);
24// }
25// ```
26// ->
27// ```
28// fn frobnicate() {}
29//
30// fn main() {
31//     frobnicate();
32// }
33// ```
34pub(crate) fn remove_unused_param(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
35    let param: ast::Param = ctx.find_node_at_offset()?;
36    let ident_pat = match param.pat()? {
37        ast::Pat::IdentPat(it) => it,
38        _ => return None,
39    };
40    let func = param.syntax().ancestors().find_map(ast::Fn::cast)?;
41    let is_self_present =
42        param.syntax().parent()?.children().find_map(ast::SelfParam::cast).is_some();
43
44    // check if fn is in impl Trait for ..
45    if func
46        .syntax()
47        .parent() // AssocItemList
48        .and_then(|x| x.parent())
49        .and_then(ast::Impl::cast)
50        .is_some_and(|imp| imp.trait_().is_some())
51    {
52        cov_mark::hit!(trait_impl);
53        return None;
54    }
55
56    let mut param_position = func.param_list()?.params().position(|it| it == param)?;
57    // param_list() does not take the self param into consideration, hence this additional check
58    // is required. For associated functions, param_position is incremented here. For inherent
59    // calls we revet the increment below, in process_usage, as those calls will not have an
60    // explicit self parameter.
61    if is_self_present {
62        param_position += 1;
63    }
64    let fn_def = {
65        let func = ctx.sema.to_def(&func)?;
66        Definition::Function(func)
67    };
68
69    let param_def = {
70        let local = ctx.sema.to_def(&ident_pat)?;
71        Definition::Local(local)
72    };
73    if param_def.usages(&ctx.sema).at_least_one() {
74        cov_mark::hit!(keep_used);
75        return None;
76    }
77    let parent = param.syntax().parent()?;
78    acc.add(
79        AssistId::refactor("remove_unused_param"),
80        "Remove unused parameter",
81        param.syntax().text_range(),
82        |builder| {
83            let mut editor = builder.make_editor(&parent);
84            let elements = elements_to_remove(param.syntax());
85            for element in elements {
86                editor.delete(element);
87            }
88            for (file_id, references) in fn_def.usages(&ctx.sema).all() {
89                process_usages(ctx, builder, file_id, references, param_position, is_self_present);
90            }
91            builder.add_file_edits(ctx.vfs_file_id(), editor);
92        },
93    )
94}
95
96fn process_usages(
97    ctx: &AssistContext<'_>,
98    builder: &mut SourceChangeBuilder,
99    editioned_file_id: EditionedFileId,
100    references: Vec<FileReference>,
101    arg_to_remove: usize,
102    is_self_present: bool,
103) {
104    let source_file = ctx.sema.parse(editioned_file_id);
105    let file_id = editioned_file_id.file_id(ctx.db());
106    builder.edit_file(file_id);
107    let possible_ranges = references
108        .into_iter()
109        .filter_map(|usage| process_usage(&source_file, usage, arg_to_remove, is_self_present));
110
111    for element_range in possible_ranges {
112        let Some(SyntaxElement::Node(parent)) = element_range
113            .iter()
114            .cloned()
115            .reduce(|a, b| least_common_ancestor_element(&a, &b).unwrap().syntax_element())
116        else {
117            continue;
118        };
119        let mut editor = builder.make_editor(&parent);
120        for element in element_range {
121            editor.delete(element);
122        }
123
124        builder.add_file_edits(file_id, editor);
125    }
126}
127
128fn process_usage(
129    source_file: &SourceFile,
130    FileReference { range, .. }: FileReference,
131    mut arg_to_remove: usize,
132    is_self_present: bool,
133) -> Option<Vec<SyntaxElement>> {
134    let call_expr_opt: Option<ast::CallExpr> = find_node_at_range(source_file.syntax(), range);
135    if let Some(call_expr) = call_expr_opt {
136        let call_expr_range = call_expr.expr()?.syntax().text_range();
137        if !call_expr_range.contains_range(range) {
138            return None;
139        }
140
141        let arg = call_expr.arg_list()?.args().nth(arg_to_remove)?;
142        return Some(elements_to_remove(arg.syntax()));
143    }
144
145    let method_call_expr_opt: Option<ast::MethodCallExpr> =
146        find_node_at_range(source_file.syntax(), range);
147    if let Some(method_call_expr) = method_call_expr_opt {
148        let method_call_expr_range = method_call_expr.name_ref()?.syntax().text_range();
149        if !method_call_expr_range.contains_range(range) {
150            return None;
151        }
152
153        if is_self_present {
154            arg_to_remove -= 1;
155        }
156
157        let arg = method_call_expr.arg_list()?.args().nth(arg_to_remove)?;
158        return Some(elements_to_remove(arg.syntax()));
159    }
160
161    None
162}
163
164pub(crate) fn range_to_remove(node: &SyntaxNode) -> TextRange {
165    let up_to_comma = next_prev().find_map(|dir| {
166        node.siblings_with_tokens(dir)
167            .filter_map(|it| it.into_token())
168            .find(|it| it.kind() == T![,])
169            .map(|it| (dir, it))
170    });
171    if let Some((dir, token)) = up_to_comma {
172        if node.next_sibling().is_some() {
173            let up_to_space = token
174                .siblings_with_tokens(dir)
175                .skip(1)
176                .take_while(|it| it.kind() == WHITESPACE)
177                .last()
178                .and_then(|it| it.into_token());
179            return node
180                .text_range()
181                .cover(up_to_space.map_or(token.text_range(), |it| it.text_range()));
182        }
183        node.text_range().cover(token.text_range())
184    } else {
185        node.text_range()
186    }
187}
188
189pub(crate) fn elements_to_remove(node: &SyntaxNode) -> Vec<SyntaxElement> {
190    let up_to_comma = next_prev().find_map(|dir| {
191        node.siblings_with_tokens(dir)
192            .filter_map(|it| it.into_token())
193            .find(|it| it.kind() == T![,])
194            .map(|it| (dir, it))
195    });
196    if let Some((dir, token)) = up_to_comma {
197        let after = token.siblings_with_tokens(dir).nth(1).unwrap();
198        let mut result: Vec<_> =
199            node.siblings_with_tokens(dir).take_while(|it| it != &after).collect();
200        if node.next_sibling().is_some() {
201            result.extend(
202                token.siblings_with_tokens(dir).skip(1).take_while(|it| it.kind() == WHITESPACE),
203            );
204        }
205
206        result
207    } else {
208        vec![node.syntax_element()]
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::tests::{check_assist, check_assist_not_applicable};
215
216    use super::*;
217
218    #[test]
219    fn remove_unused() {
220        check_assist(
221            remove_unused_param,
222            r#"
223fn a() { foo(9, 2) }
224fn foo(x: i32, $0y: i32) { x; }
225fn b() { foo(9, 2,) }
226"#,
227            r#"
228fn a() { foo(9) }
229fn foo(x: i32) { x; }
230fn b() { foo(9, ) }
231"#,
232        );
233    }
234
235    #[test]
236    fn remove_unused_first_param() {
237        check_assist(
238            remove_unused_param,
239            r#"
240fn foo($0x: i32, y: i32) { y; }
241fn a() { foo(1, 2) }
242fn b() { foo(1, 2,) }
243"#,
244            r#"
245fn foo(y: i32) { y; }
246fn a() { foo(2) }
247fn b() { foo(2,) }
248"#,
249        );
250    }
251
252    #[test]
253    fn remove_unused_single_param() {
254        check_assist(
255            remove_unused_param,
256            r#"
257fn foo($0x: i32) { 0; }
258fn a() { foo(1) }
259fn b() { foo(1, ) }
260"#,
261            r#"
262fn foo() { 0; }
263fn a() { foo() }
264fn b() { foo( ) }
265"#,
266        );
267    }
268
269    #[test]
270    fn remove_unused_surrounded_by_params() {
271        check_assist(
272            remove_unused_param,
273            r#"
274fn foo(x: i32, $0y: i32, z: i32) { x; }
275fn a() { foo(1, 2, 3) }
276fn b() { foo(1, 2, 3,) }
277"#,
278            r#"
279fn foo(x: i32, z: i32) { x; }
280fn a() { foo(1, 3) }
281fn b() { foo(1, 3,) }
282"#,
283        );
284    }
285
286    #[test]
287    fn remove_unused_qualified_call() {
288        check_assist(
289            remove_unused_param,
290            r#"
291mod bar { pub fn foo(x: i32, $0y: i32) { x; } }
292fn b() { bar::foo(9, 2) }
293"#,
294            r#"
295mod bar { pub fn foo(x: i32) { x; } }
296fn b() { bar::foo(9) }
297"#,
298        );
299    }
300
301    #[test]
302    fn remove_unused_turbofished_func() {
303        check_assist(
304            remove_unused_param,
305            r#"
306pub fn foo<T>(x: T, $0y: i32) { x; }
307fn b() { foo::<i32>(9, 2) }
308"#,
309            r#"
310pub fn foo<T>(x: T) { x; }
311fn b() { foo::<i32>(9) }
312"#,
313        );
314    }
315
316    #[test]
317    fn remove_unused_generic_unused_param_func() {
318        check_assist(
319            remove_unused_param,
320            r#"
321pub fn foo<T>(x: i32, $0y: T) { x; }
322fn b() { foo::<i32>(9, 2) }
323fn b2() { foo(9, 2) }
324"#,
325            r#"
326pub fn foo<T>(x: i32) { x; }
327fn b() { foo::<i32>(9) }
328fn b2() { foo(9) }
329"#,
330        );
331    }
332
333    #[test]
334    fn keep_used() {
335        cov_mark::check!(keep_used);
336        check_assist_not_applicable(
337            remove_unused_param,
338            r#"
339fn foo(x: i32, $0y: i32) { y; }
340fn main() { foo(9, 2) }
341"#,
342        );
343    }
344
345    #[test]
346    fn trait_impl() {
347        cov_mark::check!(trait_impl);
348        check_assist_not_applicable(
349            remove_unused_param,
350            r#"
351trait Trait {
352    fn foo(x: i32);
353}
354impl Trait for () {
355    fn foo($0x: i32) {}
356}
357"#,
358        );
359    }
360
361    #[test]
362    fn remove_across_files() {
363        check_assist(
364            remove_unused_param,
365            r#"
366//- /main.rs
367fn foo(x: i32, $0y: i32) { x; }
368
369mod foo;
370
371//- /foo.rs
372use super::foo;
373
374fn bar() {
375    let _ = foo(1, 2);
376}
377"#,
378            r#"
379//- /main.rs
380fn foo(x: i32) { x; }
381
382mod foo;
383
384//- /foo.rs
385use super::foo;
386
387fn bar() {
388    let _ = foo(1);
389}
390"#,
391        )
392    }
393
394    #[test]
395    fn test_remove_method_param() {
396        check_assist(
397            remove_unused_param,
398            r#"
399struct S;
400impl S { fn f(&self, $0_unused: i32) {} }
401fn main() {
402    S.f(92);
403    S.f();
404    S.f(93, 92);
405    S::f(&S, 92);
406}
407"#,
408            r#"
409struct S;
410impl S { fn f(&self) {} }
411fn main() {
412    S.f();
413    S.f();
414    S.f(92);
415    S::f(&S);
416}
417"#,
418        )
419    }
420
421    #[test]
422    fn nested_call() {
423        check_assist(
424            remove_unused_param,
425            r#"
426fn foo(x: i32, $0y: i32) -> i32 {
427    x
428}
429
430fn bar() {
431    foo(1, foo(2, 3));
432}
433"#,
434            r#"
435fn foo(x: i32) -> i32 {
436    x
437}
438
439fn bar() {
440    foo(1);
441}
442"#,
443        )
444    }
445}