Skip to content

Commit a3a3715

Browse files
feat(linter): implement noUnassignedVariables rule (#6219)
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
1 parent c5217cf commit a3a3715

File tree

17 files changed

+564
-61
lines changed

17 files changed

+564
-61
lines changed

‎.changeset/silent-buckets-stop.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added new nursery rule [`noUnassignedVariables`](https://biomejs.dev/linter/rules/no-unassigned-variables/), which disallows `let` or `var` variables that are read but never assigned.
6+
7+
The following code is now reported as invalid:
8+
9+
```js
10+
let x;
11+
if (x) {
12+
console.log(1);
13+
}
14+
```
15+
16+
The following code is now reported as valid:
17+
18+
```js
19+
let x = 1;
20+
if (x) {
21+
console.log(1);
22+
}
23+
```

‎crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/biome_configuration/src/analyzer/linter/rules.rs‎

Lines changed: 85 additions & 60 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/biome_diagnostics_categories/src/categories.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ define_categories! {
174174
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
175175
"lint/nursery/noShorthandPropertyOverrides": "https://biomejs.dev/linter/rules/no-shorthand-property-overrides",
176176
"lint/nursery/noTsIgnore": "https://biomejs.dev/linter/rules/no-ts-ignore",
177+
"lint/nursery/noUnassignedVariables": "https://biomejs.dev/linter/rules/no-unassigned-variables",
177178
"lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies",
178179
"lint/nursery/noUnknownAtRule": "https://biomejs.dev/linter/rules/no-unknown-at-rule",
179180
"lint/nursery/noUnknownFunction": "https://biomejs.dev/linter/rules/no-unknown-function",

‎crates/biome_js_analyze/src/lint/nursery.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod no_restricted_elements;
1919
pub mod no_secrets;
2020
pub mod no_shadow;
2121
pub mod no_ts_ignore;
22+
pub mod no_unassigned_variables;
2223
pub mod no_unresolved_imports;
2324
pub mod no_unwanted_polyfillio;
2425
pub mod no_useless_backref_in_regex;
@@ -43,4 +44,4 @@ pub mod use_single_js_doc_asterisk;
4344
pub mod use_sorted_classes;
4445
pub mod use_symbol_description;
4546
pub mod use_unique_element_ids;
46-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
47+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unassigned_variables :: NoUnassignedVariables , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
2+
use biome_console::markup;
3+
use biome_js_semantic::ReferencesExtensions;
4+
use biome_js_syntax::{
5+
AnyJsBinding, AnyJsBindingPattern, JsIdentifierBinding, JsVariableDeclaration,
6+
JsVariableDeclarationClause, JsVariableDeclarator, JsVariableDeclaratorList,
7+
TsDeclareStatement,
8+
};
9+
use biome_rowan::AstNode;
10+
11+
use crate::services::semantic::Semantic;
12+
13+
declare_lint_rule! {
14+
/// Disallow `let` or `var` variables that are read but never assigned.
15+
///
16+
/// This rule flags let or var declarations that are never assigned a value but are still read or used in the code.
17+
/// Since these variables will always be undefined, their usage is likely a programming mistake.
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```js,expect_diagnostic
24+
/// let status;
25+
/// if (status === 'ready') {
26+
/// console.log('Status is ready');
27+
/// }
28+
/// ```
29+
///
30+
/// ```ts,expect_diagnostic
31+
/// let value: number | undefined;
32+
/// console.log(value);
33+
/// ```
34+
///
35+
/// ### Valid
36+
///
37+
/// ```js
38+
/// let message = "hello";
39+
/// console.log(message);
40+
///
41+
/// let user;
42+
/// user = getUser();
43+
/// console.log(user.name);
44+
///
45+
/// let count;
46+
/// count = 0;
47+
/// count++;
48+
/// ```
49+
///
50+
/// ```ts
51+
/// declare let value: number | undefined;
52+
/// console.log(value);
53+
///
54+
/// declare module "my-module" {
55+
/// let value: string;
56+
/// export = value;
57+
/// }
58+
/// ```
59+
///
60+
pub NoUnassignedVariables {
61+
version: "next",
62+
name: "noUnassignedVariables",
63+
language: "js",
64+
sources: &[RuleSource::Eslint("no-unassigned-vars")],
65+
recommended: false,
66+
}
67+
}
68+
69+
impl Rule for NoUnassignedVariables {
70+
type Query = Semantic<JsVariableDeclarator>;
71+
type State = JsIdentifierBinding;
72+
type Signals = Option<Self::State>;
73+
type Options = ();
74+
75+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
76+
let declarator = ctx.query();
77+
let Ok(AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding(id))) =
78+
declarator.id()
79+
else {
80+
return None;
81+
};
82+
let declaration = declarator
83+
.parent::<JsVariableDeclaratorList>()?
84+
.parent::<JsVariableDeclaration>()?;
85+
if declaration.is_const() || declarator.initializer().is_some() {
86+
return None;
87+
}
88+
// e.g. `declare let value: number | undefined;`
89+
if declaration
90+
.parent::<JsVariableDeclarationClause>()
91+
.is_some_and(|clause| clause.parent::<TsDeclareStatement>().is_some())
92+
{
93+
return None;
94+
}
95+
// check if the variable is declared in a function or module
96+
// e.g. `declare module "my-module" { let value: string; export = value; }`
97+
if is_inside_ts_declare_statement(&declaration) {
98+
return None;
99+
}
100+
let model = ctx.model();
101+
102+
if id.all_writes(model).next().is_some() {
103+
return None;
104+
}
105+
if id.all_reads(model).next().is_some() {
106+
return Some(id);
107+
}
108+
109+
None
110+
}
111+
112+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
113+
// let state = state.to_trimmed_text();
114+
let node = ctx.query();
115+
let name_token = state.name_token().ok()?;
116+
let name = name_token.text_trimmed();
117+
Some(
118+
RuleDiagnostic::new(
119+
rule_category!(),
120+
node.range(),
121+
markup! {
122+
"The variable '"<Emphasis>{name}</Emphasis>"' is declared but never assigned a value."
123+
},
124+
)
125+
.note(markup! {
126+
"Variable declared without assignment. Either assign a value or remove the declaration."
127+
}),
128+
)
129+
}
130+
}
131+
132+
fn is_inside_ts_declare_statement(node: &JsVariableDeclaration) -> bool {
133+
node.syntax()
134+
.ancestors()
135+
.skip(1)
136+
.any(|ancestor| TsDeclareStatement::can_cast(ancestor.kind()))
137+
}

‎crates/biome_js_analyze/src/options.rs‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
let status;
2+
if (status === 'ready') {
3+
console.log('Ready!');
4+
}
5+
6+
let user;
7+
greet(user);
8+
9+
function test() {
10+
let error;
11+
return error || "Unknown error";
12+
}
13+
14+
let options;
15+
const { debug } = options || {};
16+
17+
let flag;
18+
while (!flag) {
19+
// Do something...
20+
}
21+
22+
let config;
23+
function init() {
24+
return config?.enabled;
25+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```js
7+
let status;
8+
if (status === 'ready') {
9+
console.log('Ready!');
10+
}
11+
12+
let user;
13+
greet(user);
14+
15+
function test() {
16+
let error;
17+
return error || "Unknown error";
18+
}
19+
20+
let options;
21+
const { debug } = options || {};
22+
23+
let flag;
24+
while (!flag) {
25+
// Do something...
26+
}
27+
28+
let config;
29+
function init() {
30+
return config?.enabled;
31+
}
32+
```
33+
34+
# Diagnostics
35+
```
36+
invalid.js:1:5 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
37+
38+
i The variable 'status' is declared but never assigned a value.
39+
40+
> 1let status;
41+
^^^^^^
42+
2if (status === 'ready') {
43+
3 │ console.log('Ready!');
44+
45+
i Variable declared without assignment. Either assign a value or remove the declaration.
46+
47+
48+
```
49+
50+
```
51+
invalid.js:6:5 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52+
53+
i The variable 'user' is declared but never assigned a value.
54+
55+
4 │ }
56+
5
57+
> 6let user;
58+
^^^^
59+
7greet(user);
60+
8
61+
62+
i Variable declared without assignment. Either assign a value or remove the declaration.
63+
64+
65+
```
66+
67+
```
68+
invalid.js:10:7 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69+
70+
i The variable 'error' is declared but never assigned a value.
71+
72+
9function test() {
73+
> 10let error;
74+
^^^^^
75+
11return error || "Unknown error";
76+
12 │ }
77+
78+
i Variable declared without assignment. Either assign a value or remove the declaration.
79+
80+
81+
```
82+
83+
```
84+
invalid.js:14:5 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85+
86+
i The variable 'options' is declared but never assigned a value.
87+
88+
12}
89+
13 │
90+
> 14 │ let options;
91+
│ ^^^^^^^
92+
15 │ const { debug } = options || {};
93+
16 │
94+
95+
i Variable declared without assignment. Either assign a value or remove the declaration.
96+
97+
98+
```
99+
100+
```
101+
invalid.js:17:5 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102+
103+
i The variable 'flag' is declared but never assigned a value.
104+
105+
15 │ const { debug } = options || {};
106+
16 │
107+
> 17 │ let flag;
108+
│ ^^^^
109+
18 │ while (!flag) {
110+
19// Do something...
111+
112+
i Variable declared without assignment. Either assign a value or remove the declaration.
113+
114+
115+
```
116+
117+
```
118+
invalid.js:22:5 lint/nursery/noUnassignedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
119+
120+
i The variable 'config' is declared but never assigned a value.
121+
122+
20}
123+
21 │
124+
> 22 │ let config;
125+
│ ^^^^^^
126+
23 │ function init() {
127+
24return config?.enabled;
128+
129+
i Variable declared without assignment. Either assign a value or remove the declaration.
130+
131+
132+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
let value: number | undefined;
2+
console.log(value);

0 commit comments

Comments
 (0)