From b102a91b15a17c3230ba8f971f9e0124773454cf Mon Sep 17 00:00:00 2001 From: Cam McHenry Date: Wed, 28 Oct 2026 22:28:47 -0400 Subject: [PATCH] perf(linter/typescript/no-wrapper-object-types): index references (#28359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Look up six wrapper names once per TypeScript file, preserving shadowing, type contexts, and heritage fix restrictions. Replace 13,199 fixture callbacks with four file callbacks plus matching-reference iteration. --- Stack created with GitHub Stacks CLI โ€ข Give Feedback ๐Ÿ’ฌ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/generated/rule_runner_impls.rs | 8 +- .../typescript/no_wrapper_object_types.rs | 75 +++++++++---------- .../typescript_no_wrapper_object_types.snap | 8 ++ .../track_linter_timings/linter_timings.snap | 2 +- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 0eda918865..ca89e20db4 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -2031,12 +2031,8 @@ impl RuleRunner for crate::rules::typescript::no_var_requires::NoVarRequires { } impl RuleRunner for crate::rules::typescript::no_wrapper_object_types::NoWrapperObjectTypes { - const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ - AstType::TSClassImplements, - AstType::TSInterfaceHeritage, - AstType::TSTypeReference, - ])); - const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; + const NODE_TYPES: Option<&AstTypesBitset> = None; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce; } impl RuleRunner diff --git a/crates/oxc_linter/src/rules/typescript/no_wrapper_object_types.rs b/crates/oxc_linter/src/rules/typescript/no_wrapper_object_types.rs index 50b9694f4a..321cf7abd1 100644 --- a/crates/oxc_linter/src/rules/typescript/no_wrapper_object_types.rs +++ b/crates/oxc_linter/src/rules/typescript/no_wrapper_object_types.rs @@ -1,10 +1,10 @@ use cow_utils::CowUtils; -use oxc_ast::{AstKind, ast::TSTypeName}; +use oxc_ast::AstKind; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::Span; -use crate::{AstNode, context::LintContext, rule::Rule}; +use crate::{context::LintContext, rule::Rule}; fn no_wrapper_object_types(ident_name: &str, span: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Do not use wrapper object types.") @@ -60,48 +60,35 @@ declare_oxc_lint!( ); impl Rule for NoWrapperObjectTypes { - fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { - let (ident_name, ident_span, reference_id) = match node.kind() { - AstKind::TSTypeReference(type_ref) => { - if let TSTypeName::IdentifierReference(type_name) = &type_ref.type_name { - (type_name.name.as_str(), type_name.span, type_name.reference_id()) - } else { - return; + fn run_once(&self, ctx: &LintContext) { + let scoping = ctx.scoping(); + for name in ["BigInt", "Boolean", "Number", "Object", "String", "Symbol"] { + let Some(reference_ids) = scoping.root_unresolved_references().get(name) else { + continue; + }; + for &reference_id in reference_ids { + let reference = scoping.get_reference(reference_id); + if reference.symbol_id().is_some() { + continue; } - } - AstKind::TSClassImplements(ts_class_implements) => { - if let TSTypeName::IdentifierReference(type_name) = &ts_class_implements.expression - { - (type_name.name.as_str(), type_name.span, type_name.reference_id()) + let node = ctx.nodes().get_node(reference.node_id()); + let AstKind::IdentifierReference(ident) = node.kind() else { + continue; + }; + let can_fix = match ctx.nodes().parent_kind(node.id()) { + AstKind::TSTypeReference(_) => true, + AstKind::TSClassImplements(_) | AstKind::TSInterfaceHeritage(_) => false, + _ => continue, + }; + + if can_fix { + ctx.diagnostic_with_fix(no_wrapper_object_types(name, ident.span), |fixer| { + fixer.replace(ident.span, name.cow_to_ascii_lowercase().to_string()) + }); } else { - return; + ctx.diagnostic(no_wrapper_object_types(name, ident.span)); } } - AstKind::TSInterfaceHeritage(ts_interface_heritage) => { - if let TSTypeName::IdentifierReference(type_name) = &ts_interface_heritage.type_name - { - (type_name.name.as_str(), type_name.span, type_name.reference_id()) - } else { - return; - } - } - _ => return, - }; - - if matches!(ident_name, "BigInt" | "Boolean" | "Number" | "Object" | "String" | "Symbol") { - if ctx.scoping().get_reference(reference_id).symbol_id().is_some() { - return; - } - - let can_fix = matches!(node.kind(), AstKind::TSTypeReference(_)); - - if can_fix { - ctx.diagnostic_with_fix(no_wrapper_object_types(ident_name, ident_span), |fixer| { - fixer.replace(ident_span, ident_name.cow_to_ascii_lowercase().to_string()) - }); - } else { - ctx.diagnostic(no_wrapper_object_types(ident_name, ident_span)); - } } } @@ -156,6 +143,11 @@ fn test() { "type Unknown = {};", "type Void = {};", "class MyClass extends Number {}", + "const value = Number(1); const object = new Object();", + "type Constructor = typeof Number;", + "let value: Number.Member;", + "interface Value extends Namespace.Number {}", + "class Value implements Namespace.Number {}", " type Number = 0 | 1; let value: Number; @@ -185,6 +177,7 @@ fn test() { "class MyClass implements Number {}", "interface MyInterface extends Number {}", "type MyType = Number & String;", + "let globalValue: Number; function f() { type Number = string; let localValue: Number; }", ]; let fix = vec![ @@ -200,6 +193,8 @@ fn test() { ("type MyType = Number;", "type MyType = number;"), ("type MyType = [Number];", "type MyType = [number];"), ("type MyType = Number & String;", "type MyType = number & string;"), + ("class Value implements Number {}", "class Value implements Number {}"), + ("interface Value extends Number {}", "interface Value extends Number {}"), ]; Tester::new(NoWrapperObjectTypes::NAME, NoWrapperObjectTypes::PLUGIN, pass, fail) diff --git a/crates/oxc_linter/src/snapshots/typescript_no_wrapper_object_types.snap b/crates/oxc_linter/src/snapshots/typescript_no_wrapper_object_types.snap index b215bac3a3..54d997948f 100644 --- a/crates/oxc_linter/src/snapshots/typescript_no_wrapper_object_types.snap +++ b/crates/oxc_linter/src/snapshots/typescript_no_wrapper_object_types.snap @@ -127,3 +127,11 @@ source: crates/oxc_linter/src/tester.rs โ•ฐโ”€โ”€โ”€โ”€ help: Replace `String` with `string`. note: `String` is a boxed object type, not a primitive. Boxed types have object semantics (identity/truthiness) that can be surprising. Use `string` for values, and in `extends`/`implements` use an interface/object shape instead. + + โš  typescript(no-wrapper-object-types): Do not use wrapper object types. + โ•ญโ”€[no_wrapper_object_types.tsx:1:18] + 1 โ”‚ let globalValue: Number; function f() { type Number = string; let localValue: Number; } + ยท โ”€โ”€โ”€โ”€โ”€โ”€ + โ•ฐโ”€โ”€โ”€โ”€ + help: Replace `Number` with `number`. + note: `Number` is a boxed object type, not a primitive. Boxed types have object semantics (identity/truthiness) that can be surprising. Use `number` for values, and in `extends`/`implements` use an interface/object shape instead. diff --git a/tasks/track_linter_timings/linter_timings.snap b/tasks/track_linter_timings/linter_timings.snap index 5fdc0af328..3afac5cda2 100644 --- a/tasks/track_linter_timings/linter_timings.snap +++ b/tasks/track_linter_timings/linter_timings.snap @@ -500,7 +500,7 @@ typescript/no-unsafe-declaration-merging | 500 typescript/no-unsafe-function-type | 13199 typescript/no-useless-empty-export | 5 typescript/no-var-requires | 62606 -typescript/no-wrapper-object-types | 13199 +typescript/no-wrapper-object-types | 4 typescript/parameter-properties | 1070 typescript/prefer-as-const | 1054 typescript/prefer-enum-initializers | 23