Skip to content

Commit 68c052e

Browse files
Hiroki Okadaautofix-ci[bot]
andauthored
feat(biome_js_analyze): implement noEqualsToNull rule (#8214)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 182ecdc commit 68c052e

File tree

14 files changed

+371
-97
lines changed

14 files changed

+371
-97
lines changed

.changeset/legal-rice-clean.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the [`noEqualsToNull`](https://biomejs.dev/linter/rules/no-equals-to-null) rule, which enforces the use of `===` and `!==` for comparison with `null` instead of `==` or `!=`.
6+
7+
**Invalid:**
8+
9+
```js
10+
foo == null;
11+
foo != null;
12+
```
13+
14+
**Valid:**
15+
16+
```js
17+
foo === null;
18+
foo !== null;
19+
```

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: 117 additions & 96 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
@@ -169,6 +169,7 @@ define_categories! {
169169
"lint/nursery/noDeprecatedImports": "https://biomejs.dev/linter/rules/no-deprecated-imports",
170170
"lint/nursery/noDuplicateDependencies": "https://biomejs.dev/linter/rules/no-duplicate-dependencies",
171171
"lint/nursery/noEmptySource": "https://biomejs.dev/linter/rules/no-empty-source",
172+
"lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null",
172173
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
173174
"lint/nursery/noForIn": "https://biomejs.dev/linter/rules/no-for-in",
174175
"lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use biome_analyze::declare_lint_group;
66
pub mod no_continue;
77
pub mod no_deprecated_imports;
88
pub mod no_empty_source;
9+
pub mod no_equals_to_null;
910
pub mod no_floating_promises;
1011
pub mod no_for_in;
1112
pub mod no_import_cycles;
@@ -41,4 +42,4 @@ pub mod use_sorted_classes;
4142
pub mod use_spread;
4243
pub mod use_vue_define_macros_order;
4344
pub mod use_vue_multi_word_component_names;
44-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
45+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::JsRuleAction;
2+
use biome_analyze::{
3+
Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_diagnostics::Severity;
7+
use biome_js_factory::make;
8+
use biome_js_syntax::{
9+
AnyJsExpression, AnyJsLiteralExpression, JsBinaryExpression,
10+
JsSyntaxKind::{EQ2, NEQ},
11+
JsSyntaxToken, T,
12+
};
13+
use biome_rowan::{BatchMutationExt, SyntaxResult};
14+
use biome_rule_options::no_equals_to_null::NoEqualsToNullOptions;
15+
16+
declare_lint_rule! {
17+
/// Require the use of `===` or `!==` for comparison with `null`.
18+
///
19+
/// Comparing to `null` with `==` or `!=` may have unintended results as the
20+
/// expression evaluates to `true` when comparing `null` to `undefined`.
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// ```js,expect_diagnostic
27+
/// foo == null;
28+
/// ```
29+
///
30+
/// ```js,expect_diagnostic
31+
/// foo != null;
32+
/// ```
33+
///
34+
/// ### Valid
35+
///
36+
/// ```js
37+
/// foo === null;
38+
/// ```
39+
///
40+
/// ```js
41+
/// foo !== null;
42+
/// ```
43+
pub NoEqualsToNull {
44+
version: "next",
45+
name: "noEqualsToNull",
46+
language: "js",
47+
sources: &[RuleSource::Eslint("no-eq-null").same()],
48+
recommended: false,
49+
severity: Severity::Error,
50+
fix_kind: FixKind::Unsafe,
51+
}
52+
}
53+
54+
impl Rule for NoEqualsToNull {
55+
type Query = Ast<JsBinaryExpression>;
56+
type State = JsSyntaxToken;
57+
type Signals = Option<Self::State>;
58+
type Options = NoEqualsToNullOptions;
59+
60+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
61+
let node = ctx.query();
62+
let op = node.operator_token().ok()?;
63+
64+
if matches!(op.kind(), EQ2 | NEQ)
65+
&& (is_null_literal(&node.left()) || is_null_literal(&node.right()))
66+
{
67+
return Some(op);
68+
}
69+
70+
None
71+
}
72+
73+
fn diagnostic(_ctx: &RuleContext<Self>, op: &Self::State) -> Option<RuleDiagnostic> {
74+
let text_trimmed = op.text_trimmed();
75+
Some(RuleDiagnostic::new(
76+
rule_category!(),
77+
op.text_trimmed_range(),
78+
markup! {
79+
""<Emphasis>"null"</Emphasis>" comparison with "<Emphasis>{text_trimmed}</Emphasis>" is disallowed."
80+
},
81+
))
82+
}
83+
84+
fn action(ctx: &RuleContext<Self>, op: &Self::State) -> Option<JsRuleAction> {
85+
let mut mutation = ctx.root().begin();
86+
87+
let suggestion = if op.kind() == EQ2 { T![===] } else { T![!==] };
88+
mutation.replace_token(op.clone(), make::token(suggestion));
89+
90+
Some(JsRuleAction::new(
91+
ctx.metadata().action_category(ctx.category(), ctx.group()),
92+
ctx.metadata().applicability(),
93+
markup! { "Use "<Emphasis>{suggestion.to_string()?}</Emphasis>" instead." }.to_owned(),
94+
mutation,
95+
))
96+
}
97+
}
98+
99+
fn is_null_literal(res: &SyntaxResult<AnyJsExpression>) -> bool {
100+
matches!(
101+
res,
102+
Ok(AnyJsExpression::AnyJsLiteralExpression(
103+
AnyJsLiteralExpression::JsNullLiteralExpression(_)
104+
))
105+
)
106+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* should generate diagnostics */
2+
foo == null;
3+
foo != null;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```js
7+
/* should generate diagnostics */
8+
foo == null;
9+
foo != null;
10+
11+
```
12+
13+
# Diagnostics
14+
```
15+
invalid.js:2:5 lint/nursery/noEqualsToNull FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
× null comparison with == is disallowed.
18+
19+
1 │ /* should generate diagnostics */
20+
> 2 │ foo == null;
21+
│ ^^
22+
3 │ foo != null;
23+
4 │
24+
25+
i Unsafe fix: Use === instead.
26+
27+
2 │ foo·===·null;
28+
│ +
29+
30+
```
31+
32+
```
33+
invalid.js:3:5 lint/nursery/noEqualsToNull FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34+
35+
× null comparison with != is disallowed.
36+
37+
1 │ /* should generate diagnostics */
38+
2 │ foo == null;
39+
> 3 │ foo != null;
40+
│ ^^
41+
4 │
42+
43+
i Unsafe fix: Use !== instead.
44+
45+
3 │ foo·!==·null;
46+
│ +
47+
48+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* should not generate diagnostics */
2+
foo === null;
3+
foo !== null;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: valid.js
4+
---
5+
# Input
6+
```js
7+
/* should not generate diagnostics */
8+
foo === null;
9+
foo !== null;
10+
11+
```

0 commit comments

Comments
 (0)