Skip to content

Commit 09ff9a5

Browse files
committed
Fix unused variable check to detect write-only variables
Assignment targets (e.g. `x = 2`) were incorrectly marked as "used", so variables that were written to but never read weren't flagged. Now the check correctly distinguishes reads from writes: only variable reads count as uses. For variables with assignments, no autofix is offered since neither removing the let nor renaming would be safe (both would leave dangling assignment references). https://claude.ai/code/session_01QdfZKyPKuP6bvX4A2r3pWw
1 parent 75ab4a3 commit 09ff9a5

File tree

4 files changed

+115
-14
lines changed

4 files changed

+115
-14
lines changed

src/checks/unused_vars.rs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rustc_hash::FxHashMap;
1+
use rustc_hash::{FxHashMap, FxHashSet};
22

33
use crate::diagnostics::{Autofix, Diagnostic, Severity};
44
use crate::parser::ast::{
@@ -49,8 +49,9 @@ struct UnusedLocalVar {
4949
name: SymbolName,
5050
/// Position of the variable name (for the warning).
5151
name_position: Position,
52-
/// How to fix this unused variable.
53-
fix: UnusedVarFix,
52+
/// How to fix this unused variable. `None` when no safe autofix
53+
/// is available (e.g. variable is assigned to after binding).
54+
fix: Option<UnusedVarFix>,
5455
}
5556

5657
/// The type of fix to apply for an unused variable.
@@ -85,6 +86,12 @@ struct UnusedVariableVisitor {
8586
/// Key is the interned symbol ID.
8687
let_removal_positions: FxHashMap<InternedSymbolId, Position>,
8788

89+
/// Variables that have been assigned to after their initial binding.
90+
/// We can't safely offer autofixes for these (removing the `let`
91+
/// would leave dangling assignments, and renaming would miss the
92+
/// assignment targets).
93+
assigned_variables: FxHashSet<InternedSymbolId>,
94+
8895
method_this_type_hint: Option<TypeHint>,
8996

9097
/// Type parameter tracking for the current function/method.
@@ -112,6 +119,7 @@ impl UnusedVariableVisitor {
112119
file_bindings: FxHashMap::default(),
113120
unused: vec![],
114121
let_removal_positions: FxHashMap::default(),
122+
assigned_variables: FxHashSet::default(),
115123
method_this_type_hint: None,
116124
type_param_info: vec![],
117125
unused_type_params: vec![],
@@ -123,17 +131,18 @@ impl UnusedVariableVisitor {
123131

124132
// Unused local variables
125133
for unused_var in &self.unused {
126-
let fix = match &unused_var.fix {
127-
UnusedVarFix::Rename => Autofix {
134+
let fixes = match &unused_var.fix {
135+
Some(UnusedVarFix::Rename) => vec![Autofix {
128136
description: format!("Rename to `_{}`", unused_var.name),
129137
position: unused_var.name_position.clone(),
130138
new_text: format!("_{}", unused_var.name),
131-
},
132-
UnusedVarFix::RemoveLet { removal_position } => Autofix {
139+
}],
140+
Some(UnusedVarFix::RemoveLet { removal_position }) => vec![Autofix {
133141
description: "Remove this let binding".to_owned(),
134142
position: removal_position.clone(),
135143
new_text: String::new(),
136-
},
144+
}],
145+
None => vec![],
137146
};
138147

139148
diagnostics.push(Diagnostic {
@@ -144,7 +153,7 @@ impl UnusedVariableVisitor {
144153
msgtext!(" is unused."),
145154
]),
146155
position: unused_var.name_position.clone(),
147-
fixes: vec![fix],
156+
fixes,
148157
});
149158
}
150159

@@ -264,11 +273,16 @@ impl UnusedVariableVisitor {
264273
}
265274

266275
if let UseState::NotUsed(position) = use_state {
267-
// Check if this is a let binding with removal info
268-
let fix = if let Some(removal_position) = self.let_removal_positions.remove(&id) {
269-
UnusedVarFix::RemoveLet { removal_position }
276+
// If this variable has been assigned to, we can't
277+
// safely offer autofixes (removing the let would
278+
// leave dangling assignments, and renaming would miss
279+
// the assignment targets).
280+
let fix = if self.assigned_variables.contains(&id) {
281+
None
282+
} else if let Some(removal_position) = self.let_removal_positions.remove(&id) {
283+
Some(UnusedVarFix::RemoveLet { removal_position })
270284
} else {
271-
UnusedVarFix::Rename
285+
Some(UnusedVarFix::Rename)
272286
};
273287

274288
self.unused.push(UnusedLocalVar {
@@ -570,7 +584,15 @@ impl Visitor for UnusedVariableVisitor {
570584
}
571585

572586
fn visit_expr_assign(&mut self, var: &Symbol, expr: &Expression) {
573-
self.check_symbol(var);
587+
// An assignment is a write, not a read. Don't mark the
588+
// variable as used, so we can detect write-only variables.
589+
if self.is_locally_bound(var) {
590+
self.assigned_variables.insert(var.interned_id);
591+
// Can't safely remove the let binding when there are
592+
// later assignments to the variable.
593+
self.let_removal_positions.remove(&var.interned_id);
594+
}
595+
574596
self.visit_expr(expr);
575597
}
576598

src/test_files/check/assign_returns_unit.gdn

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,13 @@ public fun foo(): Unit {
44
}
55

66
// args: check
7+
// expected exit status: 1
8+
// expected stdout:
9+
// Warning: `i` is unused.
10+
// -| src/test_files/check/assign_returns_unit.gdn:2:9
11+
// 1| public fun foo(): Unit {
12+
// 2| let i = 0
13+
// | ^
14+
// 3| i = 2
15+
// 4| }
16+

src/test_files/check/assign_wrong_type.gdn

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ public fun foo() {
66
// args: check
77
// expected exit status: 1
88
// expected stdout:
9+
// Warning: `x` is unused.
10+
// -| src/test_files/check/assign_wrong_type.gdn:2:9
11+
// 1| public fun foo() {
12+
// 2| let x = 1
13+
// | ^
14+
// 3| x = ""
15+
// 4| }
16+
//
917
// Error: Expected `Int`, but got `String`.
1018
// -| src/test_files/check/assign_wrong_type.gdn:3:9
1119
// 1| public fun foo() {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// A variable that is only written to (assigned) but never read
2+
// should be reported as unused.
3+
fun foo(): Int {
4+
let x = 1
5+
x = 2
6+
0
7+
}
8+
9+
// But a variable that is read after being assigned should not warn.
10+
fun bar(): Int {
11+
let x = 1
12+
x = 2
13+
x
14+
}
15+
16+
// Warning: `foo` is never called.
17+
// --| src/test_files/check/unused_assigned_var.gdn:3:5
18+
// 1| // A variable that is only written to (assigned) but never read
19+
// 2| // should be reported as unused.
20+
// 3| fun foo(): Int {
21+
// | ^^^
22+
// 4| let x = 1
23+
// 5| x = 2
24+
//
25+
// Warning: `bar` is never called.
26+
// --| src/test_files/check/unused_assigned_var.gdn:10:5
27+
// 9| // But a variable that is read after being assigned should not warn.
28+
// 10| fun bar(): Int {
29+
// | ^^^
30+
// 11| let x = 1
31+
// 12| x = 2
32+
33+
// args: check
34+
// expected exit status: 1
35+
// expected stdout:
36+
// Warning: `x` is unused.
37+
// --| src/test_files/check/unused_assigned_var.gdn:4:9
38+
// 2| // should be reported as unused.
39+
// 3| fun foo(): Int {
40+
// 4| let x = 1
41+
// | ^
42+
// 5| x = 2
43+
// 6| 0
44+
//
45+
// Warning: `foo` is never called.
46+
// --| src/test_files/check/unused_assigned_var.gdn:3:5
47+
// 1| // A variable that is only written to (assigned) but never read
48+
// 2| // should be reported as unused.
49+
// 3| fun foo(): Int {
50+
// | ^^^
51+
// 4| let x = 1
52+
// 5| x = 2
53+
//
54+
// Warning: `bar` is never called.
55+
// --| src/test_files/check/unused_assigned_var.gdn:10:5
56+
// 9| // But a variable that is read after being assigned should not warn.
57+
// 10| fun bar(): Int {
58+
// | ^^^
59+
// 11| let x = 1
60+
// 12| x = 2
61+

0 commit comments

Comments
 (0)