fix(parser): report reserved type-declaration names in the parser (#23035)

## What

Moves the reserved built-in type name check (a type's name may not be `any`, `string`, `number`, …) out of the semantic checker into the parser, via a reusable `check_reserved_type_name` helper. Covers **all** declaration forms:

- class → TS2414
- interface → TS2427
- type alias → TS2457
- enum → TS2431
- type parameter → TS2368

The semantic `check_type_name_is_reserved` helper and its diagnostic are removed.

## Why it moves cleanly

- **Zero state:** each is a name-vs-reserved-list match at the parse site where the identifier is parsed.
- **Node-final:** these declarations are never reinterpreted by cover grammar.
- **Negative-only:** confirmed by `cargo coverage` — no positive/negative count changes; only the errors relocating in `parser_typescript.snap`.

## Class needs an `is_ts` guard + a fixture fix

`class string {}` is valid JavaScript (these names are reserved only as *type* names), so the class check is guarded by `is_ts`. The interface/type-alias/enum/type-parameter forms are TypeScript-only parser paths, so they need no guard.

The `no-shadow-restricted-names` rule had `class undefined {}` / `(class undefined {})` fixtures. Since the lint test harness parses fixtures as TSX, those are now parse errors (TS2414). They're dropped — class-name shadowing is still covered by `class globalThis {}` (globalThis isn't a reserved type name), and `undefined` shadowing by the `var`/`import` fixtures.

## Verification

- `class number {}`, `interface any {}`, `type any = number`, `enum string {}`, `function f<any>() {}` → error; `class string {}` in `.js` → clean; valid names → clean.
- Parser/semantic/linter (1155) tests pass; clippy clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-06-07 02:05:14 +00:00
1 parent 1f9d8eb131
commit cf53285466
12 files changed
+153 -89

No files matched your search

@@ -201,8 +201,6 @@ fn test() {
None,
), // { "ecmaVersion": 9 },
("var undefined; undefined = 5;", None),
("class undefined {}", None), // { "ecmaVersion": 2015, },
("(class undefined {})", None), // { "ecmaVersion": 2015, },
("import undefined from 'foo';", None), // { "ecmaVersion": 2015, "sourceType": "module", },
("import { undefined } from 'foo';", None), // { "ecmaVersion": 2015, "sourceType": "module", },
("import { baz as undefined } from 'foo';", None), // { "ecmaVersion": 2015, "sourceType": "module", },
@@ -289,20 +289,6 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Rename 'undefined' to avoid shadowing the global property.
⚠ eslint(no-shadow-restricted-names): Shadowing of global properties such as `undefined` is not allowed.
╭─[no_shadow_restricted_names.tsx:1:7]
1 │ class undefined {}
· ─────────
╰────
help: Rename 'undefined' to avoid shadowing the global property.
⚠ eslint(no-shadow-restricted-names): Shadowing of global properties such as `undefined` is not allowed.
╭─[no_shadow_restricted_names.tsx:1:8]
1 │ (class undefined {})
· ─────────
╰────
help: Rename 'undefined' to avoid shadowing the global property.
⚠ eslint(no-shadow-restricted-names): Shadowing of global properties such as `undefined` is not allowed.
╭─[no_shadow_restricted_names.tsx:1:8]
1 │ import undefined from 'foo';
+13
View File
@@ -1221,6 +1221,19 @@ pub fn import_alias_cannot_use_import_type(span: Span) -> OxcDiagnostic {
ts_error("1392", "An import alias cannot use 'import type'").with_label(span)
}
#[cold]
pub fn reserved_type_name(span: Span, reserved_name: &str, syntax_name: &str) -> OxcDiagnostic {
let code = match syntax_name {
"Type parameter" => "2368",
"Interface" => "2427",
"Enum" => "2431",
"Type alias" => "2457",
// "Class" and any other declaration form
_ => "2414",
};
ts_error(code, format!("{syntax_name} name cannot be '{reserved_name}'")).with_label(span)
}
#[cold]
pub fn abstract_accessor_cannot_have_implementation(name: &str, span: Span) -> OxcDiagnostic {
ts_error(
+7
View File
@@ -81,6 +81,13 @@ impl<'a, C: Config> ParserImpl<'a, C> {
} else {
None
};
// A class name may not be a reserved type name, but only in TypeScript
// (`class string {}` is valid JavaScript).
if self.is_ts
&& let Some(id) = &id
{
self.check_reserved_type_name(id, "Class");
}
let type_parameters = if self.is_ts { self.parse_ts_type_parameters() } else { None };
let (extends, implements) = self.parse_heritage_clause();
+28
View File
@@ -25,6 +25,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
) -> Declaration<'a> {
self.bump_any(); // bump `enum`
let id = self.parse_binding_identifier();
self.check_reserved_type_name(&id, "Enum");
let body = self.parse_ts_enum_body();
let span = self.end_span(span);
self.verify_modifiers(
@@ -129,6 +130,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
self.expect(Kind::Type);
let id = self.parse_binding_identifier();
self.check_reserved_type_name(&id, "Type alias");
let params = self.parse_ts_type_parameters();
self.expect(Kind::Eq);
@@ -173,12 +175,38 @@ impl<'a, C: Config> ParserImpl<'a, C> {
/* --------------------- Interface ------------------------ */
/// A type declaration's name may not be one of the reserved built-in type names
/// (`any`, `string`, `number`, ...).
pub(crate) fn check_reserved_type_name(
&mut self,
id: &BindingIdentifier<'a>,
syntax_name: &'static str,
) {
if matches!(
id.name.as_str(),
"any"
| "unknown"
| "never"
| "number"
| "bigint"
| "boolean"
| "string"
| "symbol"
| "void"
| "object"
| "undefined"
) {
self.error(diagnostics::reserved_type_name(id.span, &id.name, syntax_name));
}
}
pub(crate) fn parse_ts_interface_declaration(
&mut self,
span: u32,
modifiers: &Modifiers,
) -> Declaration<'a> {
let id = self.parse_binding_identifier();
self.check_reserved_type_name(&id, "Interface");
let type_parameters = self.parse_ts_type_parameters();
let (extends, implements) = self.parse_heritage_clause();
let body = self.parse_ts_interface_body();
+2
View File
@@ -210,6 +210,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
);
let name = self.parse_binding_identifier();
self.check_reserved_type_name(&name, "Type parameter");
let constraint = self.parse_ts_type_constraint();
let default = self.parse_ts_default_type();
@@ -302,6 +303,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
fn parse_type_parameter_of_infer_type(&mut self) -> Box<'a, TSTypeParameter<'a>> {
let span = self.start_span();
let name = self.parse_binding_identifier();
self.check_reserved_type_name(&name, "Type parameter");
let constraint = self.parse_constraint_of_infer_type();
let span = self.end_span(span);
-2
View File
@@ -107,12 +107,10 @@ pub fn check<'a>(kind: AstKind<'a>, ctx: &SemanticBuilder<'a>) {
js::check_variable_declarator_redeclaration(decl, ctx);
}
AstKind::TSTypeAnnotation(annot) => ts::check_ts_type_annotation(annot, ctx),
AstKind::TSInterfaceDeclaration(decl) => ts::check_ts_interface_declaration(decl, ctx),
AstKind::TSTypeParameter(param) => ts::check_ts_type_parameter(param, ctx),
AstKind::TSModuleDeclaration(decl) => ts::check_ts_module_declaration(decl, ctx),
AstKind::TSGlobalDeclaration(decl) => ts::check_ts_global_declaration(decl, ctx),
AstKind::TSEnumDeclaration(decl) => ts::check_ts_enum_declaration(decl, ctx),
AstKind::TSTypeAliasDeclaration(decl) => ts::check_ts_type_alias_declaration(decl, ctx),
AstKind::TSInferType(infer_type) => ts::check_ts_infer_type(infer_type, ctx),
_ => {}
}
@@ -11,7 +11,6 @@ use oxc_str::Str;
use crate::{builder::SemanticBuilder, diagnostics};
pub fn check_ts_type_parameter<'a>(param: &TSTypeParameter<'a>, ctx: &SemanticBuilder<'a>) {
check_type_name_is_reserved(&param.name, ctx, "Type parameter");
if param.r#in || param.out {
let is_allowed_node = matches!(
// skip parent TSTypeParameterDeclaration
@@ -65,13 +64,6 @@ pub fn check_ts_type_annotation(annotation: &TSTypeAnnotation<'_>, ctx: &Semanti
));
}
pub fn check_ts_type_alias_declaration<'a>(
decl: &TSTypeAliasDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
check_type_name_is_reserved(&decl.id, ctx, "Type alias");
}
pub fn check_ts_infer_type<'a>(infer_type: &TSInferType<'a>, ctx: &SemanticBuilder<'a>) {
let is_in_conditional_extends_clause =
ctx.nodes.ancestor_kinds(ctx.current_node_id).any(|kind| {
@@ -169,8 +161,6 @@ pub fn check_ts_enum_declaration<'a>(decl: &TSEnumDeclaration<'a>, ctx: &Semanti
ctx.error(diagnostics::enum_member_must_have_initializer(member.span));
}
});
check_type_name_is_reserved(&decl.id, ctx, "Enum");
}
pub fn check_class<'a>(class: &Class<'a>, ctx: &SemanticBuilder<'a>) {
@@ -217,50 +207,6 @@ pub fn check_class<'a>(class: &Class<'a>, ctx: &SemanticBuilder<'a>) {
}
}
}
if let Some(id) = &class.id {
check_type_name_is_reserved(id, ctx, "Class");
}
}
pub fn check_ts_interface_declaration<'a>(
decl: &TSInterfaceDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
check_type_name_is_reserved(&decl.id, ctx, "Interface");
}
/// ```ts
/// function checkTypeNameIsReserved(name: Identifier, message: DiagnosticMessage): void {
/// // TS 1.0 spec (April 2014): 3.6.1
/// // The predefined type keywords are reserved and cannot be used as names of user defined types.
/// switch (name.escapedText) {
/// case "any":
/// case "unknown":
/// case "never":
/// case "number":
/// case "bigint":
/// case "boolean":
/// case "string":
/// case "symbol":
/// case "void":
/// case "object":
/// case "undefined":
/// error(name, message, name.escapedText as string);
/// }
/// }
/// ```
fn check_type_name_is_reserved<'a>(
id: &BindingIdentifier<'a>,
ctx: &SemanticBuilder<'a>,
syntax_name: &str,
) {
match id.name.as_str() {
"any" | "unknown" | "never" | "number" | "bigint" | "boolean" | "string" | "symbol"
| "void" | "object" | "undefined" => {
ctx.error(diagnostics::reserved_type_name(id.span, id.name.as_str(), syntax_name));
}
_ => {}
}
}
pub fn check_method_definition<'a>(method: &MethodDefinition<'a>, ctx: &SemanticBuilder<'a>) {
-16
View File
@@ -367,22 +367,6 @@ pub fn function_implementation_missing(span: Span) -> OxcDiagnostic {
.with_label(span)
}
#[cold]
pub fn reserved_type_name(span: Span, reserved_name: &str, syntax_name: &str) -> OxcDiagnostic {
let code = match syntax_name {
"Type parameter" => "2368",
"Class" => "2414",
"Interface" => "2427",
"Enum" => "2431",
"Type alias" => "2457",
_ => {
debug_assert!(false, "all syntax_name should have a corresponding match arm");
"2414"
}
};
ts_error(code, format!("{syntax_name} name cannot be '{reserved_name}'")).with_label(span)
}
/// 'abstract' modifier can only appear on a class, method, or property declaration. (1242)
#[cold]
pub fn illegal_abstract_modifier(span: Span) -> OxcDiagnostic {
@@ -0,0 +1 @@
type T<X> = X extends infer string ? string : never;
+7 -1
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 68/68 (100.00%)
Positive Passed: 68/68 (100.00%)
Negative Passed: 144/144 (100.00%)
Negative Passed: 145/145 (100.00%)
× Cannot assign to 'arguments' in strict mode
╭─[misc/fail/arguments-eval.ts:1:10]
@@ -355,6 +355,12 @@ Negative Passed: 144/144 (100.00%)
· ╰── `from` expected
╰────
× TS(2368): Type parameter name cannot be 'string'
╭─[misc/fail/infer-reserved-type-name.ts:1:29]
1 │ type T<X> = X extends infer string ? string : never;
· ──────
╰────
× Unexpected JSX expression
╭─[misc/fail/jsx-in-js.js:1:20]
1 │ export const foo = <Foo />;
@@ -18125,6 +18125,38 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
╰────
help: Either remove this `await` or add the `async` keyword to the enclosing function
× TS(2431): Enum name cannot be 'any'
╭─[typescript/tests/cases/conformance/enums/enumErrors.ts:2:6]
1 │ // Enum named with PredefinedTypes
2 │ enum any { }
· ───
3 │ enum number { }
╰────
× TS(2431): Enum name cannot be 'number'
╭─[typescript/tests/cases/conformance/enums/enumErrors.ts:3:6]
2 │ enum any { }
3 │ enum number { }
· ──────
4 │ enum string { }
╰────
× TS(2431): Enum name cannot be 'string'
╭─[typescript/tests/cases/conformance/enums/enumErrors.ts:4:6]
3 │ enum number { }
4 │ enum string { }
· ──────
5 │ enum boolean { }
╰────
× TS(2431): Enum name cannot be 'boolean'
╭─[typescript/tests/cases/conformance/enums/enumErrors.ts:5:6]
4 │ enum string { }
5 │ enum boolean { }
· ───────
6 │
╰────
× Expected `,` or `}` but found `;`
╭─[typescript/tests/cases/conformance/enums/enumErrors.ts:48:18]
43 │ // Enum with incorrect syntax
@@ -23241,6 +23273,37 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
╰────
help: Only 'readonly' modifier is allowed here.
× TS(2427): Interface name cannot be 'any'
╭─[typescript/tests/cases/conformance/interfaces/interfaceDeclarations/interfacesWithPredefinedTypesAsNames.ts:1:11]
1 │ interface any { }
· ───
2 │ interface number { }
╰────
× TS(2427): Interface name cannot be 'number'
╭─[typescript/tests/cases/conformance/interfaces/interfaceDeclarations/interfacesWithPredefinedTypesAsNames.ts:2:11]
1 │ interface any { }
2 │ interface number { }
· ──────
3 │ interface string { }
╰────
× TS(2427): Interface name cannot be 'string'
╭─[typescript/tests/cases/conformance/interfaces/interfaceDeclarations/interfacesWithPredefinedTypesAsNames.ts:3:11]
2 │ interface number { }
3 │ interface string { }
· ──────
4 │ interface boolean { }
╰────
× TS(2427): Interface name cannot be 'boolean'
╭─[typescript/tests/cases/conformance/interfaces/interfaceDeclarations/interfacesWithPredefinedTypesAsNames.ts:4:11]
3 │ interface string { }
4 │ interface boolean { }
· ───────
5 │ interface void {}
╰────
× Expected a semicolon or an implicit semicolon after a statement, but found none
╭─[typescript/tests/cases/conformance/interfaces/interfaceDeclarations/interfacesWithPredefinedTypesAsNames.ts:5:10]
4 │ interface boolean { }
@@ -30292,6 +30355,38 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
15 │
╰────
× TS(2457): Type alias name cannot be 'any'
╭─[typescript/tests/cases/conformance/types/typeAliases/reservedNamesInAliases.ts:2:6]
1 │ interface I {}
2 │ type any = I;
· ───
3 │ type number = I;
╰────
× TS(2457): Type alias name cannot be 'number'
╭─[typescript/tests/cases/conformance/types/typeAliases/reservedNamesInAliases.ts:3:6]
2 │ type any = I;
3 │ type number = I;
· ──────
4 │ type boolean = I;
╰────
× TS(2457): Type alias name cannot be 'boolean'
╭─[typescript/tests/cases/conformance/types/typeAliases/reservedNamesInAliases.ts:4:6]
3 │ type number = I;
4 │ type boolean = I;
· ───────
5 │ type string = I;
╰────
× TS(2457): Type alias name cannot be 'string'
╭─[typescript/tests/cases/conformance/types/typeAliases/reservedNamesInAliases.ts:5:6]
4 │ type boolean = I;
5 │ type string = I;
· ──────
6 │ type void = I;
╰────
× Expected a semicolon or an implicit semicolon after a statement, but found none
╭─[typescript/tests/cases/conformance/types/typeAliases/reservedNamesInAliases.ts:6:5]
5 │ type string = I;