diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c02379e06..276284c54 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -17,6 +17,7 @@ oxc_macros::declare_all_lint_rules! { no_empty_pattern, no_new_symbol, no_self_compare, + no_shadow_restricted_names, no_mixed_operators, no_constant_binary_expression, no_constant_condition, diff --git a/crates/oxc_linter/src/rules/no_shadow_restricted_names.rs b/crates/oxc_linter/src/rules/no_shadow_restricted_names.rs new file mode 100644 index 000000000..157f4c15d --- /dev/null +++ b/crates/oxc_linter/src/rules/no_shadow_restricted_names.rs @@ -0,0 +1,111 @@ +use oxc_ast::{AstKind, Atom, Span}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::Symbol; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(no-shadow-restricted-names): Shadowing of global property '{0}'")] +#[diagnostic(severity(warning))] +struct NoShadowRestrictedNamesDiagnostic( + Atom, + #[label("Shadowing of global property '{0}'")] pub Span, +); + +#[derive(Debug, Default, Clone)] +pub struct NoShadowRestrictedNames; + +declare_oxc_lint!( + /// ### What it does + /// Disallow identifiers from shadowing restricted names + /// + /// ### Why is this bad? + /// ES5 §15.1.1 Value Properties of the Global Object (NaN, Infinity, undefined) as well as strict mode restricted identifiers eval and arguments are considered to be restricted names in JavaScript. + /// Defining them to mean something else can have unintended consequences and confuse others reading the code. + /// + /// ### Example + /// ```javascript + /// var undefined = "foo"; + /// ``` + NoShadowRestrictedNames, + correctness +); + +static RESTRICTED: [&str; 5] = ["undefined", "NaN", "Infinity", "arguments", "eval"]; + +fn safely_shadows_undefined(symbol: &Symbol, ctx: &LintContext<'_>) -> bool { + if symbol.name().as_str() == "undefined" { + let mut no_assign = true; + for reference_id in symbol.references() { + let reference = ctx.semantic().symbols().get_resolved_reference(*reference_id).unwrap(); + if reference.is_write() { + no_assign = false; + } + } + let mut no_init = false; + let decl = ctx.nodes()[symbol.declaration()]; + if let AstKind::VariableDeclarator(var) = decl.kind() { + no_init = var.init.is_none(); + } + return no_assign && no_init; + } + false +} + +impl Rule for NoShadowRestrictedNames { + fn run_on_symbol(&self, symbol: &Symbol, ctx: &LintContext<'_>) { + if RESTRICTED.contains(&symbol.name().as_str()) && !safely_shadows_undefined(symbol, ctx) { + ctx.diagnostic(NoShadowRestrictedNamesDiagnostic(symbol.name().clone(), symbol.span())); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("function foo(bar){ var baz; }", None), + ("!function foo(bar){ var baz; }", None), + ("!function(bar){ var baz; }", None), + ("try {} catch(e) {}", None), + ("export default function() {}", None), + ("var undefined;", None), + ("var undefined; doSomething(undefined);", None), + ("var undefined; var undefined;", None), + ("let undefined", None), + ]; + + let fail = vec![ + ("function NaN(NaN) { var NaN; !function NaN(NaN) { try {} catch(NaN) {} }; }", None), + ( + "function undefined(undefined) { !function undefined(undefined) { try {} catch(undefined) {} }; }", + None, + ), + ( + "function Infinity(Infinity) { var Infinity; !function Infinity(Infinity) { try {} catch(Infinity) {} }; }", + None, + ), + ( + "function arguments(arguments) { var arguments; !function arguments(arguments) { try {} catch(arguments) {} }; }", + None, + ), + ("function eval(eval) { var eval; !function eval(eval) { try {} catch(eval) {} }; }", None), + ( + "var eval = (eval) => { var eval; !function eval(eval) { try {} catch(eval) {} }; }", + None, + ), + ("var [undefined] = [1]", None), + ( + "var {undefined} = obj; var {a: undefined} = obj; var {a: {b: {undefined}}} = obj; var {a, ...undefined} = obj;", + None, + ), + ("var undefined; undefined = 5;", None), + ]; + + Tester::new(NoShadowRestrictedNames::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_shadow_restricted_names.snap b/crates/oxc_linter/src/snapshots/no_shadow_restricted_names.snap new file mode 100644 index 000000000..aaafbf2a9 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_shadow_restricted_names.snap @@ -0,0 +1,194 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_shadow_restricted_names +--- + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'NaN' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function NaN(NaN) { var NaN; !function NaN(NaN) { try {} catch(NaN) {} }; } + · ─┬─ + · ╰── Shadowing of global property 'NaN' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'NaN' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function NaN(NaN) { var NaN; !function NaN(NaN) { try {} catch(NaN) {} }; } + · ─┬─ + · ╰── Shadowing of global property 'NaN' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'NaN' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function NaN(NaN) { var NaN; !function NaN(NaN) { try {} catch(NaN) {} }; } + · ─┬─ + · ╰── Shadowing of global property 'NaN' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'NaN' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function NaN(NaN) { var NaN; !function NaN(NaN) { try {} catch(NaN) {} }; } + · ─┬─ + · ╰── Shadowing of global property 'NaN' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function undefined(undefined) { !function undefined(undefined) { try {} catch(undefined) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function undefined(undefined) { !function undefined(undefined) { try {} catch(undefined) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function undefined(undefined) { !function undefined(undefined) { try {} catch(undefined) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function undefined(undefined) { !function undefined(undefined) { try {} catch(undefined) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'Infinity' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function Infinity(Infinity) { var Infinity; !function Infinity(Infinity) { try {} catch(Infinity) {} }; } + · ────┬─── + · ╰── Shadowing of global property 'Infinity' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'Infinity' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function Infinity(Infinity) { var Infinity; !function Infinity(Infinity) { try {} catch(Infinity) {} }; } + · ────┬─── + · ╰── Shadowing of global property 'Infinity' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'Infinity' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function Infinity(Infinity) { var Infinity; !function Infinity(Infinity) { try {} catch(Infinity) {} }; } + · ────┬─── + · ╰── Shadowing of global property 'Infinity' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'Infinity' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function Infinity(Infinity) { var Infinity; !function Infinity(Infinity) { try {} catch(Infinity) {} }; } + · ────┬─── + · ╰── Shadowing of global property 'Infinity' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'arguments' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function arguments(arguments) { var arguments; !function arguments(arguments) { try {} catch(arguments) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'arguments' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'arguments' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function arguments(arguments) { var arguments; !function arguments(arguments) { try {} catch(arguments) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'arguments' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'arguments' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function arguments(arguments) { var arguments; !function arguments(arguments) { try {} catch(arguments) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'arguments' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'arguments' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function arguments(arguments) { var arguments; !function arguments(arguments) { try {} catch(arguments) {} }; } + · ────┬──── + · ╰── Shadowing of global property 'arguments' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function eval(eval) { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function eval(eval) { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function eval(eval) { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ function eval(eval) { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var eval = (eval) => { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var eval = (eval) => { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var eval = (eval) => { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'eval' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var eval = (eval) => { var eval; !function eval(eval) { try {} catch(eval) {} }; } + · ──┬─ + · ╰── Shadowing of global property 'eval' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var [undefined] = [1] + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var {undefined} = obj; var {a: undefined} = obj; var {a: {b: {undefined}}} = obj; var {a, ...undefined} = obj; + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── + + ⚠ eslint(no-shadow-restricted-names): Shadowing of global property 'undefined' + ╭─[no_shadow_restricted_names.tsx:1:1] + 1 │ var undefined; undefined = 5; + · ────┬──── + · ╰── Shadowing of global property 'undefined' + ╰──── +