fix(parser): report import type alias to a non-external reference in the parser (#23032)

## What

Moves the "An import alias cannot use 'import type'" check (TS1392) out of the semantic checker (`check_ts_import_equals_declaration`) into the parser's `parse_ts_import_equals_declaration`.

`import type Foo = Bar.Baz` is rejected; `import type Foo = require('./foo')` is allowed.

## Why it's a clean move

- **Zero state:** the check is `import_kind.is_type() && !module_reference.is_external()` — both already in hand at the parse site.
- **Node-final:** a `TSImportEqualsDeclaration` is never reinterpreted by cover grammar.
- **Negative-only:** confirmed by `cargo coverage` — zero snapshot/count changes in any suite.

## Notes

A parser unit test parsed `import type foo = bar` (the now-rejected form) and asserted no errors. Updated it to assert the AST is still built (the error is recoverable) alongside the diagnostic.

## Verification

- `import type Foo = Bar.Baz` → error; `import type Foo = require('./foo')` and `import Foo = Bar.Baz` → clean.
- Parser/semantic/linter tests pass; clippy clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-06-06 18:07:58 +00:00
1 parent 90180b8b37
commit 95dd3a2970
6 files changed
+25 -21

No files matched your search

+5
View File
@@ -1209,6 +1209,11 @@ pub fn abstract_with_private_identifier(span: Span) -> OxcDiagnostic {
.with_label(span)
}
#[cold]
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 abstract_accessor_cannot_have_implementation(name: &str, span: Span) -> OxcDiagnostic {
ts_error(
+16 -1
View File
@@ -1298,8 +1298,10 @@ mod test {
}
});
// `import type foo = bar` builds the AST but is a semantic error (TS1392:
// an import alias cannot use `import type`), reported by the parser.
let src = "import type foo = bar";
parse_and_assert_statements(src, |statements| {
parse_and_assert_statements_with_error(src, |statements| {
if let Statement::TSImportEqualsDeclaration(decl) = statements[0] {
assert_eq!(decl.import_kind, ImportOrExportKind::Type);
assert_eq!(decl.id.name, "foo");
@@ -1360,6 +1362,19 @@ mod test {
f(ret.program.body.iter().collect::<Vec<_>>());
}
/// Like [`parse_and_assert_statements`] but for a recoverable error: the parser
/// reports a diagnostic yet still builds the AST, which `f` asserts on.
fn parse_and_assert_statements_with_error(
src: &'static str,
f: fn(Vec<&oxc_ast::ast::Statement<'_>>) -> (),
) {
let source_type = SourceType::default().with_typescript(true);
let allocator = Allocator::default();
let ret = Parser::new(&allocator, src, source_type).parse();
assert!(!ret.errors.is_empty(), "Expected a parse error for source: {src:?}");
f(ret.program.body.iter().collect::<Vec<_>>());
}
fn parse_and_assert_import_declarations(
src: &'static str,
// takes a function which accepts the list of statements
+4
View File
@@ -651,6 +651,10 @@ impl<'a, C: Config> ParserImpl<'a, C> {
if !self.is_ts {
self.error(diagnostics::import_equals_can_only_be_used_in_typescript_files(span));
}
// `import type Foo = Bar.Baz` is not allowed; `import type Foo = require('./foo')` is.
if import_kind.is_type() && !module_reference.is_external() {
self.error(diagnostics::import_alias_cannot_use_import_type(span));
}
self.ast.declaration_ts_import_equals(span, identifier, module_reference, import_kind)
}
-3
View File
@@ -114,9 +114,6 @@ pub fn check<'a>(kind: AstKind<'a>, ctx: &SemanticBuilder<'a>) {
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),
AstKind::TSImportEqualsDeclaration(decl) => {
ts::check_ts_import_equals_declaration(decl, ctx);
}
AstKind::JSXExpressionContainer(container) => {
ts::check_jsx_expression_container(container, ctx);
}
@@ -173,17 +173,6 @@ pub fn check_ts_enum_declaration<'a>(decl: &TSEnumDeclaration<'a>, ctx: &Semanti
check_type_name_is_reserved(&decl.id, ctx, "Enum");
}
pub fn check_ts_import_equals_declaration<'a>(
decl: &TSImportEqualsDeclaration<'a>,
ctx: &SemanticBuilder<'a>,
) {
// `import type Foo = require('./foo')` is allowed
// `import { Foo } from './foo'; import type Bar = Foo.Bar` is not allowed
if decl.import_kind.is_type() && !decl.module_reference.is_external() {
ctx.error(diagnostics::import_alias_cannot_use_import_type(decl.span));
}
}
pub fn check_class<'a>(class: &Class<'a>, ctx: &SemanticBuilder<'a>) {
if !class.r#abstract {
for elem in &class.body.body {
-6
View File
@@ -334,12 +334,6 @@ pub fn enum_member_must_have_initializer(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Enum member must have initializer.").with_label(span)
}
/// TS(1392)
#[cold]
pub fn import_alias_cannot_use_import_type(span: Span) -> OxcDiagnostic {
ts_error("1392", "An import alias cannot use 'import type'").with_label(span)
}
/// 'infer' declarations are only permitted in the 'extends' clause of a conditional type. (1338)
#[cold]
pub fn infer_declaration_only_permitted_in_extends_clause(span: Span) -> OxcDiagnostic {