Skip to content

Commit 457aa31

Browse files
authored
feat(linter): implement no-lone-blocks rule (#8145)
implement: https://eslint.org/docs/latest/rules/no-lone-blocks
1 parent 372eb09 commit 457aa31

File tree

3 files changed

+609
-0
lines changed

3 files changed

+609
-0
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ mod eslint {
9292
pub mod no_iterator;
9393
pub mod no_label_var;
9494
pub mod no_labels;
95+
pub mod no_lone_blocks;
9596
pub mod no_loss_of_precision;
9697
pub mod no_magic_numbers;
9798
pub mod no_multi_assign;
@@ -548,6 +549,7 @@ oxc_macros::declare_all_lint_rules! {
548549
eslint::no_multi_assign,
549550
eslint::no_nested_ternary,
550551
eslint::no_labels,
552+
eslint::no_lone_blocks,
551553
eslint::no_restricted_imports,
552554
eslint::no_object_constructor,
553555
eslint::no_duplicate_imports,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
use oxc_ast::ast::{Declaration, VariableDeclarationKind};
2+
use oxc_ast::AstKind;
3+
use oxc_diagnostics::OxcDiagnostic;
4+
use oxc_macros::declare_oxc_lint;
5+
use oxc_span::{GetSpan, Span};
6+
7+
use crate::{context::LintContext, rule::Rule, AstNode};
8+
9+
fn no_lone_blocks_diagnostic(span: Span) -> OxcDiagnostic {
10+
OxcDiagnostic::warn("Block is unnecessary.").with_label(span)
11+
}
12+
13+
fn no_nested_lone_blocks_diagnostic(span: Span) -> OxcDiagnostic {
14+
OxcDiagnostic::warn("Nested block is redundant.").with_label(span)
15+
}
16+
17+
#[derive(Debug, Default, Clone)]
18+
pub struct NoLoneBlocks;
19+
20+
declare_oxc_lint!(
21+
/// ### What it does
22+
///
23+
/// Disallows unnecessary standalone block statements.
24+
///
25+
/// ### Why is this bad?
26+
///
27+
/// Standalone blocks can be confusing as they do not provide any meaningful purpose when used unnecessarily.
28+
/// They may introduce extra nesting, reducing code readability, and can mislead readers about scope or intent.
29+
///
30+
/// ### Examples
31+
///
32+
/// Examples of **incorrect** code for this rule:
33+
/// ```js
34+
/// {
35+
/// var x = 1;
36+
/// }
37+
/// ```
38+
///
39+
/// Examples of **correct** code for this rule:
40+
/// ```js
41+
/// if (condition) {
42+
/// var x = 1;
43+
/// }
44+
///
45+
/// {
46+
/// let x = 1; // Used to create a valid block scope.
47+
/// }
48+
/// ```
49+
NoLoneBlocks,
50+
eslint,
51+
style,
52+
);
53+
54+
impl Rule for NoLoneBlocks {
55+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
56+
let AstKind::BlockStatement(stmt) = node.kind() else {
57+
return;
58+
};
59+
60+
let Some(parent_node) = ctx.nodes().parent_node(node.id()) else {
61+
return;
62+
};
63+
64+
let body = &stmt.body;
65+
if body.is_empty() {
66+
report(ctx, node, parent_node);
67+
return;
68+
}
69+
70+
if body.len() == 1
71+
&& matches!(parent_node.kind(), AstKind::FunctionBody(parent) if parent.statements.len() == 1)
72+
{
73+
report(ctx, node, parent_node);
74+
}
75+
76+
let mut lone_blocks = Vec::new();
77+
if is_lone_block(node, parent_node) {
78+
lone_blocks.push(node);
79+
}
80+
81+
for child in &stmt.body {
82+
match child.as_declaration() {
83+
Some(Declaration::VariableDeclaration(decl))
84+
if decl.kind != VariableDeclarationKind::Var =>
85+
{
86+
mark_lone_block(node, &mut lone_blocks);
87+
}
88+
Some(Declaration::ClassDeclaration(_) | Declaration::FunctionDeclaration(_)) => {
89+
mark_lone_block(node, &mut lone_blocks);
90+
}
91+
_ => {}
92+
}
93+
}
94+
95+
if let Some(last) = lone_blocks.last() {
96+
if last.id() == node.id() {
97+
lone_blocks.pop();
98+
report(ctx, node, parent_node);
99+
}
100+
} else {
101+
match parent_node.kind() {
102+
AstKind::BlockStatement(parent_statement) => {
103+
if parent_statement.body.len() == 1 {
104+
report(ctx, node, parent_node);
105+
}
106+
}
107+
AstKind::StaticBlock(parent_statement) => {
108+
if parent_statement.body.len() == 1 {
109+
report(ctx, node, parent_node);
110+
}
111+
}
112+
_ => {}
113+
}
114+
}
115+
}
116+
}
117+
118+
fn report(ctx: &LintContext, node: &AstNode, parent_node: &AstNode) {
119+
match parent_node.kind() {
120+
AstKind::BlockStatement(_) | AstKind::StaticBlock(_) => {
121+
ctx.diagnostic(no_nested_lone_blocks_diagnostic(node.span()));
122+
}
123+
_ => ctx.diagnostic(no_lone_blocks_diagnostic(node.span())),
124+
};
125+
}
126+
127+
fn is_lone_block(node: &AstNode, parent_node: &AstNode) -> bool {
128+
match parent_node.kind() {
129+
AstKind::BlockStatement(_) | AstKind::StaticBlock(_) | AstKind::Program(_) => true,
130+
AstKind::SwitchCase(parent_node) => {
131+
let consequent = &parent_node.consequent;
132+
if consequent.len() != 1 {
133+
return true;
134+
}
135+
let node_span = node.span();
136+
let consequent_span = consequent[0].span();
137+
node_span.start != consequent_span.start || node_span.end != consequent_span.end
138+
}
139+
_ => false,
140+
}
141+
}
142+
143+
fn mark_lone_block(parent_node: &AstNode, lone_blocks: &mut Vec<&AstNode>) {
144+
if lone_blocks.is_empty() {
145+
return;
146+
}
147+
148+
if lone_blocks.last().is_some_and(|last| last.id() == parent_node.id()) {
149+
lone_blocks.pop();
150+
}
151+
}
152+
153+
#[test]
154+
fn test() {
155+
use crate::tester::Tester;
156+
157+
let pass = vec![
158+
"if (foo) { if (bar) { baz(); } }",
159+
"do { bar(); } while (foo)",
160+
"function foo() { while (bar) { baz() } }",
161+
"{ let x = 1; }", // { "ecmaVersion": 6 },
162+
"{ const x = 1; }", // { "ecmaVersion": 6 },
163+
"'use strict'; { function bar() {} }", // { "ecmaVersion": 6 },
164+
"{ function bar() {} }", // { "ecmaVersion": 6, "parserOptions": { "ecmaFeatures": { "impliedStrict": true } } },
165+
"{ class Bar {} }", // { "ecmaVersion": 6 },
166+
"{ {let y = 1;} let x = 1; }", // { "ecmaVersion": 6 },
167+
"
168+
switch (foo) {
169+
case bar: {
170+
baz;
171+
}
172+
}
173+
",
174+
"
175+
switch (foo) {
176+
case bar: {
177+
baz;
178+
}
179+
case qux: {
180+
boop;
181+
}
182+
}
183+
",
184+
"
185+
switch (foo) {
186+
case bar:
187+
{
188+
baz;
189+
}
190+
}
191+
",
192+
"function foo() { { const x = 4 } const x = 3 }", // { "ecmaVersion": 6 },
193+
"class C { static {} }", // { "ecmaVersion": 2022 },
194+
"class C { static { foo; } }", // { "ecmaVersion": 2022 },
195+
"class C { static { if (foo) { block; } } }", // { "ecmaVersion": 2022 },
196+
"class C { static { lbl: { block; } } }", // { "ecmaVersion": 2022 },
197+
"class C { static { { let block; } something; } }", // { "ecmaVersion": 2022 },
198+
"class C { static { something; { const block = 1; } } }", // { "ecmaVersion": 2022 },
199+
"class C { static { { function block(){} } something; } }", // { "ecmaVersion": 2022 },
200+
"class C { static { something; { class block {} } } }", // { "ecmaVersion": 2022 },
201+
"
202+
{
203+
using x = makeDisposable();
204+
}", // { "parser": require(parser("typescript-parsers/no-lone-blocks/using")), "ecmaVersion": 2022 },
205+
"
206+
{
207+
await using x = makeDisposable();
208+
}", // { "parser": require(parser("typescript-parsers/no-lone-blocks/await-using")), "ecmaVersion": 2022 }
209+
];
210+
211+
let fail = vec![
212+
"{}",
213+
"{var x = 1;}",
214+
"foo(); {} bar();",
215+
"if (foo) { bar(); {} baz(); }",
216+
"{
217+
{ } }",
218+
"function foo() { bar(); {} baz(); }",
219+
"while (foo) { {} }",
220+
// MEMO: Currently, this rule always analyzes in strict mode (as it cannot retrieve ecmaFeatures).
221+
// "{ function bar() {} }", // { "ecmaVersion": 6 },
222+
"{var x = 1;}", // { "ecmaVersion": 6 },
223+
"{
224+
{var x = 1;}
225+
let y = 2; } {let z = 1;}", // { "ecmaVersion": 6 },
226+
"{
227+
{let x = 1;}
228+
var y = 2; } {let z = 1;}", // { "ecmaVersion": 6 },
229+
"{
230+
{var x = 1;}
231+
var y = 2; }
232+
{var z = 1;}", // { "ecmaVersion": 6 },
233+
"
234+
switch (foo) {
235+
case 1:
236+
foo();
237+
{
238+
bar;
239+
}
240+
}
241+
",
242+
"
243+
switch (foo) {
244+
case 1:
245+
{
246+
bar;
247+
}
248+
foo();
249+
}
250+
",
251+
"
252+
function foo () {
253+
{
254+
const x = 4;
255+
}
256+
}
257+
", // { "ecmaVersion": 6 },
258+
"
259+
function foo () {
260+
{
261+
var x = 4;
262+
}
263+
}
264+
",
265+
"
266+
class C {
267+
static {
268+
if (foo) {
269+
{
270+
let block;
271+
}
272+
}
273+
}
274+
}
275+
", // { "ecmaVersion": 2022 },
276+
"
277+
class C {
278+
static {
279+
if (foo) {
280+
{
281+
block;
282+
}
283+
something;
284+
}
285+
}
286+
}
287+
", // { "ecmaVersion": 2022 },
288+
"
289+
class C {
290+
static {
291+
{
292+
block;
293+
}
294+
}
295+
}
296+
", // { "ecmaVersion": 2022 },
297+
"
298+
class C {
299+
static {
300+
{
301+
let block;
302+
}
303+
}
304+
}
305+
", // { "ecmaVersion": 2022 },
306+
"
307+
class C {
308+
static {
309+
{
310+
const block = 1;
311+
}
312+
}
313+
}
314+
", // { "ecmaVersion": 2022 },
315+
"
316+
class C {
317+
static {
318+
{
319+
function block() {}
320+
}
321+
}
322+
}
323+
", // { "ecmaVersion": 2022 },
324+
"
325+
class C {
326+
static {
327+
{
328+
class block {}
329+
}
330+
}
331+
}
332+
", // { "ecmaVersion": 2022 },
333+
"
334+
class C {
335+
static {
336+
{
337+
var block;
338+
}
339+
something;
340+
}
341+
}
342+
", // { "ecmaVersion": 2022 },
343+
"
344+
class C {
345+
static {
346+
something;
347+
{
348+
var block;
349+
}
350+
}
351+
}
352+
", // { "ecmaVersion": 2022 },
353+
"
354+
class C {
355+
static {
356+
{
357+
block;
358+
}
359+
something;
360+
}
361+
}
362+
", // { "ecmaVersion": 2022 },
363+
"
364+
class C {
365+
static {
366+
something;
367+
{
368+
block;
369+
}
370+
}
371+
}
372+
", // { "ecmaVersion": 2022 }
373+
];
374+
375+
Tester::new(NoLoneBlocks::NAME, NoLoneBlocks::PLUGIN, pass, fail).test_and_snapshot();
376+
}

0 commit comments

Comments
 (0)