mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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';
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(¶m.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>) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user