ide_diagnostics/handlers/
unused_variables.rs

1use hir::Name;
2use ide_db::text_edit::TextEdit;
3use ide_db::{
4    FileRange, RootDatabase,
5    assists::{Assist, AssistId},
6    label::Label,
7    source_change::SourceChange,
8};
9use syntax::{AstNode, Edition, TextRange, ToSmolStr};
10
11use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext};
12
13// Diagnostic: unused-variables
14//
15// This diagnostic is triggered when a local variable is not used.
16pub(crate) fn unused_variables(
17    ctx: &DiagnosticsContext<'_>,
18    d: &hir::UnusedVariable,
19) -> Option<Diagnostic> {
20    let ast = d.local.primary_source(ctx.sema.db).syntax_ptr();
21    if ast.file_id.macro_file().is_some() {
22        // FIXME: Our infra can't handle allow from within macro expansions rn
23        return None;
24    }
25    let diagnostic_range = ctx.sema.diagnostics_display_range(ast);
26    // The range for the Actual Name. We don't want to replace the entire declaration. Using the diagnostic range causes issues within in Array Destructuring.
27    let primary_source = d.local.primary_source(ctx.sema.db);
28    let name_range = primary_source
29        .name()
30        .map(|v| v.syntax().original_file_range_rooted(ctx.sema.db))
31        .filter(|it| {
32            Some(it.file_id) == ast.file_id.file_id()
33                && diagnostic_range.range.contains_range(it.range)
34        });
35    let is_shorthand_field = primary_source
36        .source
37        .value
38        .left()
39        .and_then(|name| name.syntax().parent())
40        .and_then(syntax::ast::RecordPatField::cast)
41        .is_some_and(|field| field.colon_token().is_none());
42    let var_name = d.local.name(ctx.sema.db);
43    Some(
44        Diagnostic::new_with_syntax_node_ptr(
45            ctx,
46            DiagnosticCode::RustcLint("unused_variables"),
47            "unused variable",
48            ast,
49        )
50        .with_fixes(name_range.and_then(|it| {
51            fixes(
52                ctx.sema.db,
53                var_name,
54                it.range,
55                diagnostic_range,
56                ast.file_id.is_macro(),
57                is_shorthand_field,
58                ctx.edition,
59            )
60        })),
61    )
62}
63
64fn fixes(
65    db: &RootDatabase,
66    var_name: Name,
67    name_range: TextRange,
68    diagnostic_range: FileRange,
69    is_in_marco: bool,
70    is_shorthand_field: bool,
71    edition: Edition,
72) -> Option<Vec<Assist>> {
73    if is_in_marco {
74        return None;
75    }
76    let name = var_name.display(db, edition).to_smolstr();
77    let name = name.strip_prefix("r#").unwrap_or(&name);
78    let new_name = if is_shorthand_field { format!("{name}: _{name}") } else { format!("_{name}") };
79
80    Some(vec![Assist {
81        id: AssistId::quick_fix("unscore_unused_variable_name"),
82        label: Label::new(format!("Rename unused {name} to {new_name}")),
83        group: None,
84        target: diagnostic_range.range,
85        source_change: Some(SourceChange::from_text_edit(
86            diagnostic_range.file_id,
87            TextEdit::replace(name_range, new_name),
88        )),
89        command: None,
90    }])
91}
92
93#[cfg(test)]
94mod tests {
95    use crate::tests::{check_diagnostics, check_fix};
96
97    #[test]
98    fn unused_variables_simple() {
99        check_diagnostics(
100            r#"
101//- minicore: fn
102struct Foo { f1: i32, f2: i64 }
103
104fn f(kkk: i32) {}
105   //^^^ 💡 warn: unused variable
106fn main() {
107    let a = 2;
108      //^ 💡 warn: unused variable
109    let b = 5;
110    // note: `unused variable` implies `unused mut`, so we should not emit both at the same time.
111    let mut c = f(b);
112      //^^^^^ 💡 warn: unused variable
113    let (d, e) = (3, 5);
114       //^ 💡 warn: unused variable
115    let _ = e;
116    let f1 = 2;
117    let f2 = 5;
118    let f = Foo { f1, f2 };
119    match f {
120        Foo { f1, f2 } => {
121            //^^ 💡 warn: unused variable
122            _ = f2;
123        }
124    }
125    let g = false;
126    if g {}
127    let h: fn() -> i32 = || 2;
128    let i = h();
129      //^ 💡 warn: unused variable
130}
131"#,
132        );
133    }
134
135    #[test]
136    fn unused_self() {
137        check_diagnostics(
138            r#"
139struct S {
140}
141impl S {
142    fn owned_self(self, u: i32) {}
143                      //^ 💡 warn: unused variable
144    fn ref_self(&self, u: i32) {}
145                     //^ 💡 warn: unused variable
146    fn ref_mut_self(&mut self, u: i32) {}
147                             //^ 💡 warn: unused variable
148    fn owned_mut_self(mut self) {}
149                    //^^^^^^^^ 💡 warn: variable does not need to be mutable
150
151}
152"#,
153        );
154    }
155
156    #[test]
157    fn allow_unused_variables_for_identifiers_starting_with_underline() {
158        check_diagnostics(
159            r#"
160fn main() {
161    let _x = 2;
162}
163"#,
164        );
165    }
166
167    #[test]
168    fn respect_lint_attributes_for_unused_variables() {
169        check_diagnostics(
170            r#"
171fn main() {
172    #[allow(unused_variables)]
173    let x = 2;
174}
175
176#[deny(unused)]
177fn main2() {
178    let x = 2;
179      //^ 💡 error: unused variable
180}
181"#,
182        );
183    }
184
185    #[test]
186    fn apply_last_lint_attribute_when_multiple_are_present() {
187        check_diagnostics(
188            r#"
189#![allow(unused_variables)]
190#![warn(unused_variables)]
191#![deny(unused_variables)]
192
193fn main() {
194    let x = 2;
195      //^ 💡 error: unused variable
196
197    #[deny(unused_variables)]
198    #[warn(unused_variables)]
199    #[allow(unused_variables)]
200    let y = 0;
201}
202"#,
203        );
204    }
205
206    #[test]
207    fn prefer_closest_ancestor_lint_attribute() {
208        check_diagnostics(
209            r#"
210#![allow(unused_variables)]
211
212fn main() {
213    #![warn(unused_variables)]
214
215    #[deny(unused_variables)]
216    let x = 2;
217      //^ 💡 error: unused variable
218}
219
220#[warn(unused_variables)]
221fn main2() {
222    #[deny(unused_variables)]
223    let x = 2;
224      //^ 💡 error: unused variable
225}
226
227#[warn(unused_variables)]
228fn main3() {
229    let x = 2;
230      //^ 💡 warn: unused variable
231}
232
233fn main4() {
234    let x = 2;
235}
236"#,
237        );
238    }
239
240    #[test]
241    fn fix_unused_variable() {
242        check_fix(
243            r#"
244fn main() {
245    let x$0 = 2;
246}
247"#,
248            r#"
249fn main() {
250    let _x = 2;
251}
252"#,
253        );
254
255        check_fix(
256            r#"
257fn main() {
258    let ($0d, _e) = (3, 5);
259}
260"#,
261            r#"
262fn main() {
263    let (_d, _e) = (3, 5);
264}
265"#,
266        );
267
268        check_fix(
269            r#"
270struct Foo { f1: i32, f2: i64 }
271fn main() {
272    let f = Foo { f1: 0, f2: 0 };
273    match f {
274        Foo { f1$0, f2 } => {
275            _ = f2;
276        }
277    }
278}
279"#,
280            r#"
281struct Foo { f1: i32, f2: i64 }
282fn main() {
283    let f = Foo { f1: 0, f2: 0 };
284    match f {
285        Foo { f1: _f1, f2 } => {
286            _ = f2;
287        }
288    }
289}
290"#,
291        );
292
293        check_fix(
294            r#"
295fn main() {
296    let $0r#type = 2;
297}
298"#,
299            r#"
300fn main() {
301    let _type = 2;
302}
303"#,
304        );
305    }
306
307    #[test]
308    fn no_fix_for_marco() {
309        check_diagnostics(
310            r#"
311macro_rules! my_macro {
312    () => {
313        let x = 3;
314    };
315}
316
317fn main() {
318    my_macro!();
319}
320"#,
321        );
322    }
323    #[test]
324    fn unused_variable_in_array_destructure() {
325        check_fix(
326            r#"
327fn main() {
328    let arr = [1, 2, 3, 4, 5];
329    let [_x, y$0 @ ..] = arr;
330}
331"#,
332            r#"
333fn main() {
334    let arr = [1, 2, 3, 4, 5];
335    let [_x, _y @ ..] = arr;
336}
337"#,
338        );
339    }
340
341    #[test]
342    fn unused_variable_in_record_field() {
343        check_fix(
344            r#"
345struct S { field : u32 }
346fn main() {
347    let s = S { field : 2 };
348    let S { field: $0x } = s
349}
350"#,
351            r#"
352struct S { field : u32 }
353fn main() {
354    let s = S { field : 2 };
355    let S { field: _x } = s
356}
357"#,
358        );
359    }
360
361    #[test]
362    fn unused_variable_in_shorthand_record_field() {
363        check_fix(
364            r#"
365struct S { field : u32 }
366fn main() {
367    let s = S { field : 2 };
368    let S { $0field } = s
369}
370"#,
371            r#"
372struct S { field : u32 }
373fn main() {
374    let s = S { field : 2 };
375    let S { field: _field } = s
376}
377"#,
378        );
379    }
380
381    // regression test as we used to panic in this scenario
382    #[test]
383    fn unknown_struct_pattern_param_type() {
384        check_diagnostics(
385            r#"
386struct S { field : u32 }
387fn f(S { field }: error) {
388      // ^^^^^ 💡 warn: unused variable
389}
390"#,
391        );
392    }
393}