diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1769b6ebb838f..cfdd7b23302e1 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -143,6 +143,7 @@ mod eslint { pub mod no_ternary; pub mod no_this_before_super; pub mod no_throw_literal; + pub mod no_unassigned_vars; pub mod no_undef; pub mod no_undefined; pub mod no_unexpected_multiline; @@ -601,6 +602,7 @@ oxc_macros::declare_all_lint_rules! { eslint::max_nested_callbacks, eslint::max_params, eslint::new_cap, + eslint::no_unassigned_vars, eslint::no_extra_bind, eslint::no_alert, eslint::no_array_constructor, diff --git a/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs b/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs new file mode 100644 index 0000000000000..52c32fb76c66d --- /dev/null +++ b/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs @@ -0,0 +1,149 @@ +use oxc_ast::{AstKind, ast::BindingPatternKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{AstNode, context::LintContext, rule::Rule}; + +fn no_unassigned_vars_diagnostic(span: Span, ident_name: &str) -> OxcDiagnostic { + OxcDiagnostic::warn(format!( + "'{ident_name}' is always 'undefined' because it's never assigned.", + )) + .with_help( + "Variable declared without assignment. Either assign a value or remove the declaration.", + ) + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoUnassignedVars; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow let or var variables that are read but never assigned + /// + /// ### Why is this bad? + /// + /// This rule flags let or var declarations that are never assigned a value but are still read or used in the code. + /// Since these variables will always be undefined, their usage is likely a programming mistake. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// let status; + /// if (status === 'ready') { + /// console.log('Ready!'); + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// let message = "hello"; + /// console.log(message); + /// + /// let user; + /// user = getUser(); + /// console.log(user.name); + /// ``` + NoUnassignedVars, + eslint, + suspicious, +); + +impl Rule for NoUnassignedVars { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::VariableDeclarator(declarator) = node.kind() else { + return; + }; + if declarator.init.is_some() || declarator.kind.is_const() { + return; + } + let AstKind::VariableDeclaration(parent) = ctx.nodes().parent_kind(node.id()) else { + return; + }; + if parent.declare { + return; + } + if ctx + .nodes() + .ancestors(node.id()) + .skip(1) + .any(|ancestor| matches!(ancestor.kind(), AstKind::TSModuleDeclaration(_))) + { + return; + } + let BindingPatternKind::BindingIdentifier(ident) = &declarator.id.kind else { + return; + }; + let symbol_id = ident.symbol_id(); + let mut has_read = false; + for reference in ctx.symbol_references(symbol_id) { + if reference.is_write() { + return; + } + if reference.is_read() { + has_read = true; + } + } + if has_read { + ctx.diagnostic(no_unassigned_vars_diagnostic(ident.span, ident.name.as_str())); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "let x;", + "var x;", + "const x = undefined; log(x);", + "let y = undefined; log(y);", + "var y = undefined; log(y);", + "let a = x, b = y; log(a, b);", + "var a = x, b = y; log(a, b);", + "const foo = (two) => { let one; if (one !== two) one = two; }", + "let z: number | undefined = undefined; log(z);", + "declare let c: string | undefined; log(c);", + " + const foo = (two: string): void => { + let one: string | undefined; + if (one !== two) { + one = two; + } + } + ", + " + declare module 'module' { + import type { T } from 'module'; + let x: T; + export = x; + } + ", + ]; + + let fail = vec![ + "let x; let a = x, b; log(x, a, b);", + "const foo = (two) => { let one; if (one === two) {} }", + "let user; greet(user);", + "function test() { let error; return error || 'Unknown error'; }", + "let options; const { debug } = options || {};", + "let flag; while (!flag) { }", + "let config; function init() { return config?.enabled; }", + "let x: number; log(x);", + "let x: number | undefined; log(x);", + "const foo = (two: string): void => { let one: string | undefined; if (one === two) {} }", + " + declare module 'module' { + let x: string; + } + let y: string; + console.log(y); + ", + ]; + + Tester::new(NoUnassignedVars::NAME, NoUnassignedVars::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap b/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap new file mode 100644 index 0000000000000..07e251e12ddf5 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap @@ -0,0 +1,88 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint(no-unassigned-vars): 'x' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let x; let a = x, b; log(x, a, b); + · ─ + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'b' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:19] + 1 │ let x; let a = x, b; log(x, a, b); + · ─ + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'one' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:28] + 1 │ const foo = (two) => { let one; if (one === two) {} } + · ─── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'user' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let user; greet(user); + · ──── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'error' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:23] + 1 │ function test() { let error; return error || 'Unknown error'; } + · ───── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'options' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let options; const { debug } = options || {}; + · ─────── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'flag' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let flag; while (!flag) { } + · ──── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'config' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let config; function init() { return config?.enabled; } + · ────── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'x' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let x: number; log(x); + · ───────── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'x' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:5] + 1 │ let x: number | undefined; log(x); + · ───────────────────── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'one' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:1:42] + 1 │ const foo = (two: string): void => { let one: string | undefined; if (one === two) {} } + · ─────────────────────── + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration. + + ⚠ eslint(no-unassigned-vars): 'y' is always 'undefined' because it's never assigned. + ╭─[no_unassigned_vars.tsx:5:12] + 4 │ } + 5 │ let y: string; + · ───────── + 6 │ console.log(y); + ╰──── + help: Variable declared without assignment. Either assign a value or remove the declaration.