Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): Implement no-undef-init #6267

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/oxc_linter/src/ast_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use oxc_semantic::{AstNode, IsGlobalReference, NodeId, SymbolId};
use oxc_span::{GetSpan, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator};

use crate::context::LintContext;
#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;

use crate::context::LintContext;
use oxc_syntax::scope::ScopeId;

/// Test if an AST node is a boolean value that never changes. Specifically we
/// test for:
Expand Down Expand Up @@ -400,6 +400,10 @@ pub fn is_function_node(node: &AstNode) -> bool {
}
}

pub fn is_shadowed<'a>(scope_id: ScopeId, name: &'a str, ctx: &LintContext<'a>) -> bool {
ctx.scopes().find_binding(scope_id, name).is_some()
}

pub fn get_function_like_declaration<'b>(
node: &AstNode<'b>,
ctx: &LintContext<'b>,
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ mod eslint {
pub mod no_ternary;
pub mod no_this_before_super;
pub mod no_undef;
pub mod no_undef_init;
pub mod no_undefined;
pub mod no_unexpected_multiline;
pub mod no_unreachable;
Expand Down Expand Up @@ -578,6 +579,7 @@ oxc_macros::declare_all_lint_rules! {
eslint::no_ternary,
eslint::no_this_before_super,
eslint::no_undef,
eslint::no_undef_init,
eslint::no_undefined,
eslint::no_unexpected_multiline,
eslint::no_unreachable,
Expand Down
5 changes: 1 addition & 4 deletions crates/oxc_linter/src/rules/eslint/no_alert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use oxc_macros::declare_oxc_lint;
use oxc_semantic::ScopeId;
use oxc_span::{GetSpan, Span};

use crate::ast_util::is_shadowed;
use crate::{context::LintContext, rule::Rule, AstNode};

fn no_alert_diagnostic(span: Span) -> OxcDiagnostic {
Expand Down Expand Up @@ -83,10 +84,6 @@ fn is_global_this_ref_or_global_window<'a>(
false
}

fn is_shadowed<'a>(scope_id: ScopeId, name: &'a str, ctx: &LintContext<'a>) -> bool {
ctx.scopes().find_binding(scope_id, name).is_some()
}

impl Rule for NoAlert {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let AstKind::CallExpression(call_expr) = node.kind() else {
Expand Down
144 changes: 144 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_undef_init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use crate::ast_util::is_shadowed;
use crate::{context::LintContext, rule::Rule, AstNode};
use oxc_ast::ast::{VariableDeclarationKind, VariableDeclarator};
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

fn no_undef_init(name: &str, span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("It's not necessary to initialize {name} to undefined."))
.with_label(span.label("A variable that is declared and not initialized to any value automatically gets the value of undefined."))
}

#[derive(Debug, Default, Clone)]
pub struct NoUndefInit;

declare_oxc_lint!(
/// ### What it does
///
/// Disallow initializing variables to `undefined`.
///
/// ### Why is this bad?
///
/// A variable that is declared and not initialized to any value automatically gets the value of `undefined`.
/// It’s considered a best practice to avoid initializing variables to `undefined`.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// var a = undefined;
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// var a;
/// ```
NoUndefInit,
style,
fix
);

impl Rule for NoUndefInit {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
if let AstKind::VariableDeclaration(var_decl) = node.kind() {
let faulty_declarations: Vec<&VariableDeclarator> = var_decl
.declarations
.iter()
.filter(|decl| {
decl.init.as_ref().is_some_and(oxc_ast::ast::Expression::is_undefined)
})
.collect::<Vec<&VariableDeclarator>>();

if var_decl.kind != VariableDeclarationKind::Const
&& !faulty_declarations.is_empty()
&& !is_shadowed(node.scope_id(), "undefined", ctx)
{
for decl in faulty_declarations {
if decl.kind == VariableDeclarationKind::Var
|| decl.id.kind.is_destructuring_pattern()
|| ctx
.semantic()
.trivias()
.has_comments_between(Span::new(decl.id.span().start, decl.span().end))
{
let diagnostic_span = Span::new(decl.id.span().end, decl.span().end);
let variable_name = &ctx.source_text()[decl.id.span()];
ctx.diagnostic(no_undef_init(variable_name, diagnostic_span));
continue;
}
let identifier = decl.id.get_binding_identifier().unwrap();
if let Some(init) = &decl.init {
let diagnostic_span = Span::new(identifier.span.end, init.span().end);
ctx.diagnostic_with_fix(
no_undef_init(identifier.name.as_str(), diagnostic_span),
|fixer| fixer.delete_range(diagnostic_span),
);
}
}
}
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
"var a;",
"const foo = undefined", // { "ecmaVersion": 6 },
"var undefined = 5; var foo = undefined;",
"class C { field = undefined; }", // { "ecmaVersion": 2022 }
];

let fail = vec![
"var a = undefined;",
"var a = undefined, b = 1;",
"var a = 1, b = undefined, c = 5;",
"var [a] = undefined;", // { "ecmaVersion": 6 },
"var {a} = undefined;", // { "ecmaVersion": 6 },
"for(var i in [1,2,3]){var a = undefined; for(var j in [1,2,3]){}}",
"let a = undefined;", // { "ecmaVersion": 6 },
"let a = undefined, b = 1;", // { "ecmaVersion": 6 },
"let a = 1, b = undefined, c = 5;", // { "ecmaVersion": 6 },
"let [a] = undefined;", // { "ecmaVersion": 6 },
"let {a} = undefined;", // { "ecmaVersion": 6 },
"for(var i in [1,2,3]){let a = undefined; for(var j in [1,2,3]){}}", // { "ecmaVersion": 6 },
"let /* comment */a = undefined;", // { "ecmaVersion": 6 },
"let a/**/ = undefined;", // { "ecmaVersion": 6 },
"let a /**/ = undefined;", // { "ecmaVersion": 6 },
"let a//
= undefined;", // { "ecmaVersion": 6 },
"let a = /**/undefined;", // { "ecmaVersion": 6 },
"let a = //
undefined;", // { "ecmaVersion": 6 },
"let a = undefined/* comment */;", // { "ecmaVersion": 6 },
"let a = undefined/* comment */, b;", // { "ecmaVersion": 6 },
"let a = undefined//comment
, b;", // { "ecmaVersion": 6 }
];

let fix = vec![
("let a = undefined;", "let a;", None),
("let a = undefined, b = 1;", "let a, b = 1;", None),
("let a = 1, b = undefined, c = 5;", "let a = 1, b, c = 5;", None),
(
"for(var i in [1,2,3]){let a = undefined; for(var j in [1,2,3]){}}",
"for(var i in [1,2,3]){let a; for(var j in [1,2,3]){}}",
None,
),
("let /* comment */a = undefined;", "let /* comment */a;", None),
("let a = undefined/* comment */;", "let a/* comment */;", None),
("let a = undefined/* comment */, b;", "let a/* comment */, b;", None),
(
"let a = undefined//comment
, b;",
"let a//comment
, b;",
None,
),
];
Tester::new(NoUndefInit::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
}
158 changes: 158 additions & 0 deletions crates/oxc_linter/src/snapshots/no_undef_init.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ var a = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ var a = undefined, b = 1;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize b to undefined.
╭─[no_undef_init.tsx:1:13]
1 │ var a = 1, b = undefined, c = 5;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize [a] to undefined.
╭─[no_undef_init.tsx:1:8]
1 │ var [a] = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize {a} to undefined.
╭─[no_undef_init.tsx:1:8]
1 │ var {a} = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:28]
1 │ for(var i in [1,2,3]){var a = undefined; for(var j in [1,2,3]){}}
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = undefined, b = 1;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize b to undefined.
╭─[no_undef_init.tsx:1:13]
1 │ let a = 1, b = undefined, c = 5;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize [a] to undefined.
╭─[no_undef_init.tsx:1:8]
1 │ let [a] = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize {a} to undefined.
╭─[no_undef_init.tsx:1:8]
1 │ let {a} = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:28]
1 │ for(var i in [1,2,3]){let a = undefined; for(var j in [1,2,3]){}}
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:19]
1 │ let /* comment */a = undefined;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a/**/ = undefined;
· ────────┬───────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a /**/ = undefined;
· ────────┬────────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ ╭─▶ let a//
2 │ ├─▶ = undefined;
· ╰──── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = /**/undefined;
· ────────┬───────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ ╭─▶ let a = //
2 │ ├─▶ undefined;
· ╰──── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = undefined/* comment */;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = undefined/* comment */, b;
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
╰────
help: Delete this code.

⚠ eslint(no-undef-init): It's not necessary to initialize a to undefined.
╭─[no_undef_init.tsx:1:6]
1 │ let a = undefined//comment
· ──────┬─────
· ╰── A variable that is declared and not initialized to any value automatically gets the value of undefined.
2 │ , b;
╰────
help: Delete this code.