Skip to content

Commit c600618

Browse files
authored
feat(lint): implement noIncrementDecrement (#7859)
1 parent 0d17b05 commit c600618

File tree

20 files changed

+919
-50
lines changed

20 files changed

+919
-50
lines changed

.changeset/cool-drinks-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noIncrementDecrement`](https://biomejs.dev/linter/rules/no-increment-decrement/), disallows the usage of the unary operators ++ and --.

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: 70 additions & 49 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
@@ -171,6 +171,7 @@ define_categories! {
171171
"lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises",
172172
"lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion",
173173
"lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles",
174+
"lint/nursery/noIncrementDecrement": "https://biomejs.dev/linter/rules/no-increment-decrement",
174175
"lint/nursery/noJsxLiterals": "https://biomejs.dev/linter/rules/no-jsx-literals",
175176
"lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword",
176177
"lint/nursery/noMisusedPromises": "https://biomejs.dev/linter/rules/no-misused-promises",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod no_deprecated_imports;
77
pub mod no_empty_source;
88
pub mod no_floating_promises;
99
pub mod no_import_cycles;
10+
pub mod no_increment_decrement;
1011
pub mod no_jsx_literals;
1112
pub mod no_misused_promises;
1213
pub mod no_next_async_client_component;
@@ -30,4 +31,4 @@ pub mod use_qwik_valid_lexical_scope;
3031
pub mod use_sorted_classes;
3132
pub mod use_vue_define_macros_order;
3233
pub mod use_vue_multi_word_component_names;
33-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , 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_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , 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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
34+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , 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_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , 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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::{JsLanguage, JsPostUpdateExpression, JsPreUpdateExpression, JsSyntaxKind};
6+
use biome_rowan::{AstNode, SyntaxNode, declare_node_union};
7+
use biome_rule_options::no_increment_decrement::NoIncrementDecrementOptions;
8+
9+
declare_lint_rule! {
10+
/// Disallows the usage of the unary operators ++ and --.
11+
///
12+
/// Because the unary ++ and -- operators are subject to automatic semicolon insertion, differences in whitespace can change semantics of source code.
13+
///
14+
/// ```js,expect_diagnostic
15+
/// let i = 10;
16+
/// let j = 20;
17+
///
18+
/// i ++
19+
/// j
20+
/// // i = 11, j = 20
21+
/// ```
22+
///
23+
/// ```js,expect_diagnostic
24+
/// let i = 10;
25+
/// let j = 20;
26+
///
27+
/// i
28+
/// ++
29+
/// j
30+
/// // i = 10, j = 21
31+
/// ```
32+
///
33+
/// ## Examples
34+
///
35+
/// ### Invalid
36+
///
37+
/// ```js,expect_diagnostic
38+
/// let foo = 0;
39+
/// foo++;
40+
/// ```
41+
///
42+
/// ```js,expect_diagnostic
43+
/// let bar = 42;
44+
/// bar--;
45+
/// ```
46+
///
47+
/// ```js,expect_diagnostic
48+
/// for (let i = 0; i < 10; i++) {
49+
/// doSomething(i);
50+
/// }
51+
/// ```
52+
///
53+
/// ```js,expect_diagnostic
54+
/// for (let i = 0; i < 10;) {
55+
/// doSomething(i);
56+
/// i++;
57+
/// }
58+
/// ```
59+
///
60+
/// ### Valid
61+
///
62+
/// ```js
63+
/// let foo = 0;
64+
/// foo += 1;
65+
/// ```
66+
///
67+
/// ```js
68+
/// let bar = 42;
69+
/// bar -= 1;
70+
/// ```
71+
///
72+
/// ```js
73+
/// for (let i = 0; i < 10; i += 1) {
74+
/// doSomething(i);
75+
/// }
76+
/// ```
77+
///
78+
/// ```js
79+
/// for (let i = 0; i < 10;) {
80+
/// doSomething(i);
81+
/// i += 1;
82+
/// }
83+
/// ```
84+
///
85+
/// ## Options
86+
///
87+
/// ### `allowForLoopAfterthoughts`
88+
///
89+
/// Allows unary operators ++ and -- in the afterthought (final expression) of a for loop.
90+
///
91+
/// Default `false`
92+
///
93+
/// ```json,options
94+
/// {
95+
/// "options": {
96+
/// "allowForLoopAfterthoughts": true
97+
/// }
98+
/// }
99+
/// ```
100+
///
101+
/// #### Invalid
102+
///
103+
/// ```js,expect_diagnostic,use_options
104+
/// for (let i = 0; i < j; j = i++) {
105+
/// doSomething(i, j);
106+
/// }
107+
/// ```
108+
///
109+
/// ```js,expect_diagnostic,use_options
110+
/// for (let i = 10; i--;) {
111+
/// doSomething(i);
112+
/// }
113+
/// ```
114+
///
115+
/// ```js,expect_diagnostic,use_options
116+
/// for (let i = 0; i < 10;) i++;
117+
/// ```
118+
///
119+
/// #### Valid
120+
///
121+
/// ```js,use_options
122+
/// for (let i = 0; i < 10; i++) {
123+
/// doSomething(i);
124+
/// }
125+
/// ```
126+
///
127+
/// ```js,use_options
128+
/// for (let i = 0, j = l; i < l; i++, j--) {
129+
/// doSomething(i, j);
130+
/// }
131+
/// ```
132+
///
133+
pub NoIncrementDecrement {
134+
version: "next",
135+
name: "noIncrementDecrement",
136+
language: "js",
137+
recommended: false,
138+
sources: &[RuleSource::Eslint("no-plusplus").same()],
139+
}
140+
}
141+
142+
declare_node_union! {
143+
pub NoIncrementDecrementQuery = JsPreUpdateExpression | JsPostUpdateExpression
144+
}
145+
146+
impl Rule for NoIncrementDecrement {
147+
type Query = Ast<NoIncrementDecrementQuery>;
148+
type State = ();
149+
type Signals = Option<Self::State>;
150+
type Options = NoIncrementDecrementOptions;
151+
152+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
153+
let node = ctx.query();
154+
155+
if ctx.options().allow_for_loop_afterthoughts && is_for_loop_afterthought(node.syntax()) {
156+
return None;
157+
}
158+
159+
Some(())
160+
}
161+
162+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
163+
let node = ctx.query();
164+
Some(
165+
RuleDiagnostic::new(
166+
rule_category!(),
167+
node.range(),
168+
markup! {
169+
"Unexpected use of increment/decrement unary operator."
170+
},
171+
)
172+
.note(markup! {
173+
"The unary ++ and -- operators are subject to automatic semicolon insertion, differences in whitespace can change semantics of source code. Instead use += or -=."
174+
}),
175+
)
176+
}
177+
}
178+
179+
fn is_for_loop_afterthought(node: &SyntaxNode<JsLanguage>) -> bool {
180+
let Some(parent) = node.parent() else {
181+
return false;
182+
};
183+
184+
match parent.kind() {
185+
JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION | JsSyntaxKind::JS_SEQUENCE_EXPRESSION => {
186+
is_for_loop_afterthought(&parent)
187+
}
188+
JsSyntaxKind::JS_FOR_STATEMENT => {
189+
if let Some(for_stmt) = biome_js_syntax::JsForStatement::cast(parent.clone())
190+
&& let Some(update) = for_stmt.update()
191+
{
192+
return update.syntax().eq(node);
193+
}
194+
false
195+
}
196+
_ => false,
197+
}
198+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
let foo = 0;
2+
foo++;
3+
4+
let bar = 0;
5+
++bar;
6+
7+
for (let i = 0; i < 10; j = i++) {
8+
doSomething(i, j);
9+
}
10+
11+
for (let i = 10; i--;) {
12+
doSomething(i);
13+
}
14+
15+
for (let i = 0; i < 10;) i++;
16+
17+
for (i = 0; i < l; i++) { v++; }
18+
19+
for (i++;;);
20+
21+
for (;--i;);
22+
23+
for (;;) ++i;
24+
25+
for (;; i = j++);
26+
27+
for (;; i++, f(--j));
28+
29+
for (;; foo + (i++, bar));

0 commit comments

Comments
 (0)