Skip to content
Merged
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
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
149 changes: 149 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs
Original file line number Diff line number Diff line change
@@ -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();
}
88 changes: 88 additions & 0 deletions crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap
Original file line number Diff line number Diff line change
@@ -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.
Loading