Skip to content

Commit a69a0ee

Browse files
huangtiandi1999yefanautofix-ci[bot]camc314
authored
feat(linter): add eslint/block-scoped-var (#10237)
Relates to #479 Rule detail:https://eslint.org/docs/latest/rules/block-scoped-var --------- Co-authored-by: yefan <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cameron <[email protected]>
1 parent 387af3a commit a69a0ee

File tree

3 files changed

+653
-0
lines changed

3 files changed

+653
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod import {
3838

3939
mod eslint {
4040
pub mod array_callback_return;
41+
pub mod block_scoped_var;
4142
pub mod curly;
4243
pub mod default_case;
4344
pub mod default_case_last;
@@ -561,6 +562,7 @@ oxc_macros::declare_all_lint_rules! {
561562
// import::no_deprecated,
562563
// import::no_unused_modules,
563564
eslint::array_callback_return,
565+
eslint::block_scoped_var,
564566
eslint::curly,
565567
eslint::default_case,
566568
eslint::default_case_last,
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
use oxc_ast::{AstKind, ast::VariableDeclarationKind};
2+
use oxc_diagnostics::OxcDiagnostic;
3+
use oxc_macros::declare_oxc_lint;
4+
use oxc_span::Span;
5+
6+
use crate::{AstNode, context::LintContext, rule::Rule};
7+
8+
fn block_scoped_var_diagnostic(span: Span, name: &str) -> OxcDiagnostic {
9+
OxcDiagnostic::warn(format!("'{name}' is used outside of binding context."))
10+
.with_help(format!("Variable '{name}' is used outside its declaration block. Declare it outside the block or use 'let'/'const'."))
11+
.with_label(span)
12+
}
13+
14+
#[derive(Debug, Default, Clone)]
15+
pub struct BlockScopedVar;
16+
17+
declare_oxc_lint!(
18+
/// ### What it does
19+
///
20+
/// Generates warnings when variables are used outside of the block in which they were defined.
21+
/// This emulates C-style block scope.
22+
///
23+
/// ### Why is this bad?
24+
///
25+
/// This rule aims to reduce the usage of variables outside of their binding context
26+
/// and emulate traditional block scope from other languages.
27+
/// This is to help newcomers to the language avoid difficult bugs with variable hoisting.
28+
///
29+
/// ### Examples
30+
///
31+
/// Examples of **incorrect** code for this rule:
32+
/// ```js
33+
/// function doIf() {
34+
/// if (true) {
35+
/// var build = true;
36+
/// }
37+
/// console.log(build);
38+
/// }
39+
/// ```
40+
///
41+
/// Examples of **correct** code for this rule:
42+
/// ```js
43+
/// function doIf() {
44+
/// var build;
45+
/// if (true) {
46+
/// build = true;
47+
/// }
48+
/// console.log(build);
49+
/// }
50+
///
51+
/// ```
52+
BlockScopedVar,
53+
eslint,
54+
suspicious,
55+
);
56+
57+
impl Rule for BlockScopedVar {
58+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
59+
let AstKind::VariableDeclaration(decl) = node.kind() else {
60+
return;
61+
};
62+
if decl.kind != VariableDeclarationKind::Var {
63+
return;
64+
}
65+
let cur_node_scope_id = node.scope_id();
66+
if !ctx.scoping().scope_flags(cur_node_scope_id).is_strict_mode() {
67+
return;
68+
}
69+
// `scope_arr` contains all the scopes that are children of the current scope
70+
// we should eliminate all of them
71+
let scope_arr = ctx.scoping().iter_all_scope_child_ids(node.scope_id()).collect::<Vec<_>>();
72+
73+
let declarations = &decl.declarations;
74+
for item in declarations {
75+
let id = &item.id;
76+
// e.g. "var [a, b] = [1, 2]"
77+
for ident in id.get_binding_identifiers() {
78+
let name = ident.name.as_str();
79+
let Some(symbol_id) = ctx.scoping().find_binding(node.scope_id(), name) else {
80+
continue;
81+
};
82+
// e.g. "if (true} { var a = 4; } else { var a = 4; }"
83+
// in this case we can't find the reference of 'a' by call `get_resolved_references`
84+
// so i use `symbol_redeclarations` to find all the redeclarations
85+
for redeclaration in ctx.scoping().symbol_redeclarations(symbol_id) {
86+
let re_scope_id = ctx.nodes().get_node(redeclaration.declaration).scope_id();
87+
if !scope_arr.contains(&re_scope_id) && re_scope_id != cur_node_scope_id {
88+
ctx.diagnostic(block_scoped_var_diagnostic(redeclaration.span, name));
89+
}
90+
}
91+
// e.g. "var a = 4; console.log(a);"
92+
for reference in ctx.scoping().get_resolved_references(symbol_id) {
93+
let reference_scope_id = ctx.nodes().get_node(reference.node_id()).scope_id();
94+
if !scope_arr.contains(&reference_scope_id)
95+
&& reference_scope_id != cur_node_scope_id
96+
{
97+
ctx.diagnostic(block_scoped_var_diagnostic(
98+
ctx.reference_span(reference),
99+
name,
100+
));
101+
}
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
#[test]
109+
fn test() {
110+
use crate::tester::Tester;
111+
112+
let pass = vec![
113+
"if (1) {
114+
var a = 4;
115+
} else {
116+
let a = 4;
117+
console.log(a);
118+
}",
119+
"function f() { } f(); var exports = { f: f };",
120+
"var f = () => {}; f(); var exports = { f: f };",
121+
"!function f(){ f; }",
122+
"function f() { } f(); var exports = { f: f };",
123+
"function f() { var a, b; { a = true; } b = a; }",
124+
"var a; function f() { var b = a; }",
125+
"function f(a) { }",
126+
"!function(a) { };",
127+
"!function f(a) { };",
128+
"function f(a) { var b = a; }",
129+
"!function f(a) { var b = a; };",
130+
"function f() { var g = f; }",
131+
"function f() { } function g() { var f = g; }",
132+
"function f() { var hasOwnProperty; { hasOwnProperty; } }",
133+
"function f(){ a; b; var a, b; }",
134+
"function f(){ g(); function g(){} }",
135+
"if (true) { var a = 1; a; }",
136+
"var a; if (true) { a; }",
137+
"for (var i = 0; i < 10; i++) { i; }",
138+
"var i; for(i; i; i) { i; }",
139+
r#"
140+
function myFunc(foo) { "use strict"; var { bar } = foo; bar.hello();}
141+
"#,
142+
r#"
143+
function myFunc(foo) { "use strict"; var [ bar ] = foo; bar.hello();}
144+
"#,
145+
r#"
146+
const React = require("react/addons");const cx = React.addons.classSet;
147+
"#,
148+
r#"import * as y from "./other.js"; y();"#,
149+
"var v = 1; function x() { return v; };",
150+
"function myFunc(...foo) { return foo;}",
151+
"var f = () => { var g = f; }",
152+
"class Foo {}\nexport default Foo;",
153+
"foo; class C { static {} } var foo; ",
154+
"var foo; class C { static {} [foo]; } ",
155+
"class C { static { foo; } } var foo;",
156+
"var foo; class C { static { foo; } } ",
157+
"class C { static { if (bar) { foo; } var foo; } }",
158+
"class C { static { foo; var foo; } }",
159+
"class C { static { var foo; foo; } }",
160+
"(function () { foo(); })(); function foo() {}",
161+
"(function () { foo(); })(); function foo() {}",
162+
"foo: while (true) { bar: for (var i = 0; i < 13; ++i) {if (i === 7) break foo; } }",
163+
"foo: while (true) { bar: for (var i = 0; i < 13; ++i) {if (i === 7) continue foo; } }",
164+
"var a = { foo: bar };",
165+
"var a = { foo: foo };",
166+
"var a = { bar: 7, foo: bar };",
167+
"var a = arguments;",
168+
r#"
169+
function a(n) { return n > 0 ? b(n - 1) : "a"; } function b(n) { return n > 0 ? a(n - 1) : "b"; }
170+
"#,
171+
"const { dummy: { data, isLoading }, auth: { isLoggedIn } } = this.props;",
172+
"/*global prevState*/ const { virtualSize: prevVirtualSize = 0 } = prevState;",
173+
"/*global React*/ let {PropTypes, addons: {PureRenderMixin}} = React; let Test = React.createClass({mixins: [PureRenderMixin]});",
174+
r"
175+
function doIf() {
176+
var build;
177+
178+
if (true) {
179+
build = true;
180+
}
181+
182+
console.log(build);
183+
}
184+
",
185+
r"
186+
function doIfElse() {
187+
var build;
188+
189+
if (true) {
190+
build = true;
191+
} else {
192+
build = false;
193+
}
194+
}
195+
",
196+
r"
197+
function doTryCatch() {
198+
var build;
199+
var f;
200+
201+
try {
202+
build = 1;
203+
} catch (e) {
204+
f = build;
205+
}
206+
}
207+
",
208+
];
209+
210+
let fail = vec![
211+
r"
212+
if (true) {
213+
var a = 3;
214+
} else {
215+
var b = 4;
216+
var a = 4;
217+
}
218+
console.log(a, b);
219+
",
220+
r"
221+
if (true) {
222+
var [a, b] = [1, 2];
223+
}
224+
console.log(a, b);
225+
",
226+
r"
227+
if (true) {
228+
var a = 4, b = 3;
229+
}
230+
console.log(a, b);
231+
",
232+
r"
233+
if (true) {
234+
{
235+
var a = 4, b = 3;
236+
}
237+
var a = 4;
238+
}
239+
",
240+
"function f(){ x; { var x; } }",
241+
"function f(){ { var x; } x; }",
242+
"function f() { var a; { var b = 0; } a = b; }",
243+
"function f() { try { var a = 0; } catch (e) { var b = a; } }",
244+
"function a() { for(var b in {}) { var c = b; } c; }",
245+
"function a() { for(var b of {}) { var c = b; } c; }",
246+
"function f(){ switch(2) { case 1: var b = 2; b; break; default: b; break;} b; }",
247+
"for (var a = 0;;) {} a;",
248+
"for (var a in []) {} a;",
249+
"for (var a of []) {} a;",
250+
"{ var a = 0; } a;",
251+
"if (true) { var a; } a;",
252+
"if (true) { var a = 1; } else { var a = 2; }",
253+
"for (var i = 0;;) {} for(var i = 0;;) {}",
254+
"class C { static { if (bar) { var foo; } foo; } }",
255+
"{ var foo, bar; } bar;",
256+
"if (foo) { var a = 1; } else if (bar) { var a = 2; } else { var a = 3; }",
257+
r"
258+
if (true) {
259+
var build = true;
260+
console.log(build);
261+
{
262+
conosole.log(build);
263+
{
264+
console.log(build);
265+
}
266+
}
267+
}
268+
console.log(build);
269+
let t = build;
270+
",
271+
r"
272+
function doIf() {
273+
if (true) {
274+
var build = true;
275+
}
276+
277+
console.log(build);
278+
}
279+
",
280+
r"
281+
function doIfElse() {
282+
if (true) {
283+
var build = true;
284+
} else {
285+
var build = false;
286+
}
287+
}
288+
",
289+
r"
290+
function doTryCatch() {
291+
try {
292+
var build = 1;
293+
} catch (e) {
294+
var f = build;
295+
}
296+
}
297+
",
298+
r"
299+
function doFor() {
300+
for (var x = 1; x < 10; x++) {
301+
var y = f(x);
302+
}
303+
console.log(y);
304+
}
305+
",
306+
r"
307+
class C {
308+
static {
309+
if (something) {
310+
var build = true;
311+
}
312+
build = false;
313+
}
314+
}
315+
",
316+
];
317+
318+
Tester::new(BlockScopedVar::NAME, BlockScopedVar::PLUGIN, pass, fail).test_and_snapshot();
319+
}

0 commit comments

Comments
 (0)