From dbd8a0b7f1a101645da344bc07f4b722ac3f592f Mon Sep 17 00:00:00 2001 From: Cam McHenry Date: Wed, 28 Oct 2026 22:28:47 -0400 Subject: [PATCH] perf(linter/eslint/no-unassigned-vars): batch by declaration (#28358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Share declaration context checks across candidate bindings while preserving per-binding read/write decisions. Reduce fixture callbacks from 37,499 to 31,363. --- Stack created with GitHub Stacks CLI โ€ข Give Feedback ๐Ÿ’ฌ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/generated/rule_runner_impls.rs | 2 +- .../src/rules/eslint/no_unassigned_vars.rs | 53 +++++++++++-------- .../snapshots/eslint_no_unassigned_vars.snap | 21 ++++++++ .../track_linter_timings/linter_timings.snap | 2 +- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 9e743e2fd9..0eda918865 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1095,7 +1095,7 @@ impl RuleRunner for crate::rules::eslint::no_throw_literal::NoThrowLiteral { impl RuleRunner for crate::rules::eslint::no_unassigned_vars::NoUnassignedVars { const NODE_TYPES: Option<&AstTypesBitset> = - Some(&AstTypesBitset::from_types(&[AstType::VariableDeclarator])); + Some(&AstTypesBitset::from_types(&[AstType::VariableDeclaration])); const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } diff --git a/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs b/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs index e2104355d3..c4cb51b7fe 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unassigned_vars.rs @@ -65,27 +65,30 @@ declare_oxc_lint!( impl Rule for NoUnassignedVars { fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - let AstKind::VariableDeclarator(declarator) = node.kind() else { + let AstKind::VariableDeclaration(declaration) = node.kind() else { return; }; - if declarator.init.is_some() { + if declaration.kind.is_const() || declaration.declare { return; } - let parent_node = ctx.nodes().parent_node(node.id()); - let AstKind::VariableDeclaration(parent) = parent_node.kind() else { - return; - }; - if parent.kind.is_const() || parent.declare { + let mut identifiers = declaration + .declarations + .iter() + .filter_map(|declarator| match (&declarator.id, &declarator.init) { + (BindingPattern::BindingIdentifier(ident), None) => Some(ident), + _ => None, + }) + .peekable(); + if identifiers.peek().is_none() { return; } - let grand_parent = ctx.nodes().parent_node(parent_node.id()); if matches!( - grand_parent.kind(), + ctx.nodes().parent_kind(node.id()), AstKind::ForStatement(_) | AstKind::ForInStatement(_) | AstKind::ForOfStatement(_) ) { return; } - if ctx.nodes().ancestors(node.id()).skip(1).any(|ancestor| { + if ctx.nodes().ancestors(node.id()).any(|ancestor| { matches!( ancestor.kind(), AstKind::TSExternalModuleDeclaration(_) @@ -95,22 +98,21 @@ impl Rule for NoUnassignedVars { }) { return; } - let BindingPattern::BindingIdentifier(ident) = &declarator.id 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; + for ident in identifiers { + let mut has_read = false; + for reference in ctx.symbol_references(ident.symbol_id()) { + if reference.is_write() { + has_read = false; + break; + } + if reference.is_read() { + has_read = true; + } } - if reference.is_read() { - has_read = true; + if has_read { + ctx.diagnostic(no_unassigned_vars_diagnostic(ident.span, ident.name.as_str())); } } - if has_read { - ctx.diagnostic(no_unassigned_vars_diagnostic(ident.span, ident.name.as_str())); - } } fn should_run(&self, ctx: &crate::context::ContextHost) -> bool { @@ -150,6 +152,8 @@ fn test() { } ", "for (let p of pathToRemove) { p.remove() }", + "for (let a, b; check(a, b);) { break; }", + "declare let a, b; log(a, b);", ]; let fail = vec![ @@ -162,6 +166,9 @@ fn test() { "let config; function init() { return config?.enabled; }", "let x: number; log(x);", "let x: number | undefined; log(x);", + "let assigned, unassigned; assigned = 1; log(assigned, unassigned);", + "let a, b; log(a, b); a = 1;", + "let initialized = 1, unassigned; log(initialized, unassigned);", "const foo = (two: string): void => { let one: string | undefined; if (one === two) {} }", " declare module 'module' { diff --git a/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap b/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap index 917b34b1d3..808fdde041 100644 --- a/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap +++ b/crates/oxc_linter/src/snapshots/eslint_no_unassigned_vars.snap @@ -72,6 +72,27 @@ source: crates/oxc_linter/src/tester.rs โ•ฐโ”€โ”€โ”€โ”€ help: Variable declared without assignment. Either assign a value or remove the declaration. + โš  eslint(no-unassigned-vars): 'unassigned' is always 'undefined' because it's never assigned. + โ•ญโ”€[no_unassigned_vars.tsx:1:15] + 1 โ”‚ let assigned, unassigned; assigned = 1; log(assigned, unassigned); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + 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:8] + 1 โ”‚ let a, b; log(a, b); a = 1; + ยท โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + help: Variable declared without assignment. Either assign a value or remove the declaration. + + โš  eslint(no-unassigned-vars): 'unassigned' is always 'undefined' because it's never assigned. + โ•ญโ”€[no_unassigned_vars.tsx:1:22] + 1 โ”‚ let initialized = 1, unassigned; log(initialized, unassigned); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + 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) {} } diff --git a/tasks/track_linter_timings/linter_timings.snap b/tasks/track_linter_timings/linter_timings.snap index 7709e00b0a..5fdc0af328 100644 --- a/tasks/track_linter_timings/linter_timings.snap +++ b/tasks/track_linter_timings/linter_timings.snap @@ -121,7 +121,7 @@ eslint/no-template-curly-in-string | 62460 eslint/no-ternary | 4722 eslint/no-this-before-super | 4 eslint/no-throw-literal | 224 -eslint/no-unassigned-vars | 37499 +eslint/no-unassigned-vars | 31363 eslint/no-undef | 7 eslint/no-undefined | 282877 eslint/no-underscore-dangle | 200305