From f23f1c62a8ff82398f962425a9c33990b38a9c66 Mon Sep 17 00:00:00 2001 From: Jonathan Neal Date: Thu, 22 Jan 2026 00:34:19 -0500 Subject: [PATCH 1/4] Add support for import attributes in ambient module declarations Enables syntax: declare module "*.css" with { type: "css" } { ... } Reuses existing ImportAttributes infrastructure from import statements. --- src/compiler/emitter.ts | 5 ++ src/compiler/factory/nodeFactory.ts | 8 ++- src/compiler/parser.ts | 3 +- src/compiler/transformers/declarations.ts | 4 +- src/compiler/types.ts | 7 +- src/compiler/visitorPublic.ts | 1 + ...bientModuleWithImportAttributes.errors.txt | 48 +++++++++++++ .../ambientModuleWithImportAttributes.js | 58 ++++++++++++++++ .../ambientModuleWithImportAttributes.symbols | 55 +++++++++++++++ .../ambientModuleWithImportAttributes.types | 67 +++++++++++++++++++ tests/baselines/reference/api/typescript.d.ts | 7 +- .../ambientModuleWithImportAttributes.ts | 33 +++++++++ 12 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributes.errors.txt create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributes.js create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributes.symbols create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributes.types create mode 100644 tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 9ba1afcae27ea..297b121f08ba4 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -3635,6 +3635,11 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri } emit(node.name); + if (node.withClause) { + writeSpace(); + emit(node.withClause); + } + let body = node.body; if (!body) return writeTrailingSemicolon(); while (body && isModuleDeclaration(body)) { diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index e31d925fadd8f..cd8dd1e1d0964 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -4546,12 +4546,14 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode name: ModuleName, body: ModuleBody | undefined, flags = NodeFlags.None, + withClause?: ImportAttributes, ) { const node = createBaseDeclaration(SyntaxKind.ModuleDeclaration); node.modifiers = asNodeArray(modifiers); node.flags |= flags & (NodeFlags.Namespace | NodeFlags.NestedNamespace | NodeFlags.GlobalAugmentation); node.name = name; node.body = body; + node.withClause = withClause; if (modifiersToFlags(node.modifiers) & ModifierFlags.Ambient) { node.transformFlags = TransformFlags.ContainsTypeScript; } @@ -4575,11 +4577,13 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, + withClause?: ImportAttributes, ) { return node.modifiers !== modifiers || node.name !== name || node.body !== body - ? update(createModuleDeclaration(modifiers, name, body, node.flags), node) + || node.withClause !== withClause + ? update(createModuleDeclaration(modifiers, name, body, node.flags, withClause), node) : node; } @@ -7092,7 +7096,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode isInterfaceDeclaration(node) ? updateInterfaceDeclaration(node, modifierArray, node.name, node.typeParameters, node.heritageClauses, node.members) : isTypeAliasDeclaration(node) ? updateTypeAliasDeclaration(node, modifierArray, node.name, node.typeParameters, node.type) : isEnumDeclaration(node) ? updateEnumDeclaration(node, modifierArray, node.name, node.members) : - isModuleDeclaration(node) ? updateModuleDeclaration(node, modifierArray, node.name, node.body) : + isModuleDeclaration(node) ? updateModuleDeclaration(node, modifierArray, node.name, node.body, node.withClause) : isImportEqualsDeclaration(node) ? updateImportEqualsDeclaration(node, modifierArray, node.isTypeOnly, node.name, node.moduleReference) : isImportDeclaration(node) ? updateImportDeclaration(node, modifierArray, node.importClause, node.moduleSpecifier, node.attributes) : isExportAssignment(node) ? updateExportAssignment(node, modifierArray, node.expression) : diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 68133ac5f1e40..e782115a1e5d6 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -8324,6 +8324,7 @@ namespace Parser { name = parseLiteralNode() as StringLiteral; name.text = internIdentifier(name.text); } + const withClause = tryParseImportAttributes(); let body: ModuleBlock | undefined; if (token() === SyntaxKind.OpenBraceToken) { body = parseModuleBlock(); @@ -8331,7 +8332,7 @@ namespace Parser { else { parseSemicolon(); } - const node = factory.createModuleDeclaration(modifiersIn, name, body, flags); + const node = factory.createModuleDeclaration(modifiersIn, name, body, flags, withClause); return withJSDoc(finishNode(node, pos), hasJSDoc); } diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index 66c2dbc00ebd9..9bb6f99abf5a9 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -1360,7 +1360,7 @@ export function transformDeclarations(context: TransformationContext): Transform name: ModuleName, body: ModuleBody | undefined, ) { - const updated = factory.updateModuleDeclaration(node, modifiers, name, body); + const updated = factory.updateModuleDeclaration(node, modifiers, name, body, node.withClause); if (isAmbientModule(updated) || updated.flags & NodeFlags.Namespace) { return updated; @@ -1371,6 +1371,7 @@ export function transformDeclarations(context: TransformationContext): Transform updated.name, updated.body, updated.flags | NodeFlags.Namespace, + updated.withClause, ); setOriginalNode(fixed, updated); @@ -1512,6 +1513,7 @@ export function transformDeclarations(context: TransformationContext): Transform modifiers, namespaceDecl.name, namespaceDecl.body, + namespaceDecl.withClause, ); const exportDefaultDeclaration = factory.createExportAssignment( diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4e9f872720d79..84fb8cf1f4da8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3634,6 +3634,7 @@ export interface ModuleDeclaration extends DeclarationStatement, JSDocContainer, readonly modifiers?: NodeArray; readonly name: ModuleName; readonly body?: ModuleBody | JSDocNamespaceDeclaration; + readonly withClause?: ImportAttributes; } export type NamespaceBody = @@ -3749,7 +3750,7 @@ export interface ImportAttribute extends Node { export interface ImportAttributes extends Node { readonly token: SyntaxKind.WithKeyword | SyntaxKind.AssertKeyword; readonly kind: SyntaxKind.ImportAttributes; - readonly parent: ImportDeclaration | ExportDeclaration; + readonly parent: ImportDeclaration | ExportDeclaration | ModuleDeclaration; readonly elements: NodeArray; readonly multiLine?: boolean; } @@ -9062,8 +9063,8 @@ export interface NodeFactory { updateTypeAliasDeclaration(node: TypeAliasDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, type: TypeNode): TypeAliasDeclaration; createEnumDeclaration(modifiers: readonly ModifierLike[] | undefined, name: string | Identifier, members: readonly EnumMember[]): EnumDeclaration; updateEnumDeclaration(node: EnumDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, members: readonly EnumMember[]): EnumDeclaration; - createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags?: NodeFlags): ModuleDeclaration; - updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined): ModuleDeclaration; + createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags?: NodeFlags, withClause?: ImportAttributes): ModuleDeclaration; + updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, withClause?: ImportAttributes): ModuleDeclaration; createModuleBlock(statements: readonly Statement[]): ModuleBlock; updateModuleBlock(node: ModuleBlock, statements: readonly Statement[]): ModuleBlock; createCaseBlock(clauses: readonly CaseOrDefaultClause[]): CaseBlock; diff --git a/src/compiler/visitorPublic.ts b/src/compiler/visitorPublic.ts index d1aee94bc6e1b..d5a84d70b9cd2 100644 --- a/src/compiler/visitorPublic.ts +++ b/src/compiler/visitorPublic.ts @@ -1486,6 +1486,7 @@ const visitEachChildTable: VisitEachChildTable = { nodesVisitor(node.modifiers, visitor, isModifierLike), Debug.checkDefined(nodeVisitor(node.name, visitor, isModuleName)), nodeVisitor(node.body, visitor, isModuleBody), + nodeVisitor(node.withClause, visitor, isImportAttributes), ); }, diff --git a/tests/baselines/reference/ambientModuleWithImportAttributes.errors.txt b/tests/baselines/reference/ambientModuleWithImportAttributes.errors.txt new file mode 100644 index 0000000000000..aaa343b8fba30 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributes.errors.txt @@ -0,0 +1,48 @@ +ambientModuleWithImportAttributes.ts(2,16): error TS2664: Invalid module name in augmentation, module '*.css' cannot be found. +ambientModuleWithImportAttributes.ts(7,16): error TS2664: Invalid module name in augmentation, module '*.json' cannot be found. +ambientModuleWithImportAttributes.ts(13,16): error TS2664: Invalid module name in augmentation, module 'my-module' cannot be found. +ambientModuleWithImportAttributes.ts(19,16): error TS2664: Invalid module name in augmentation, module 'multi-attr' cannot be found. +ambientModuleWithImportAttributes.ts(24,16): error TS2664: Invalid module name in augmentation, module 'regular-module' cannot be found. + + +==== ambientModuleWithImportAttributes.ts (5 errors) ==== + // Ambient module declaration with import attributes + declare module "*.css" with { type: "css" } { + ~~~~~~~ +!!! error TS2664: Invalid module name in augmentation, module '*.css' cannot be found. + const stylesheet: CSSStyleSheet; + export default stylesheet; + } + + declare module "*.json" with { type: "json" } { + ~~~~~~~~ +!!! error TS2664: Invalid module name in augmentation, module '*.json' cannot be found. + const data: any; + export default data; + } + + // Ambient module with specific name and import attributes + declare module "my-module" with { type: "custom" } { + ~~~~~~~~~~~ +!!! error TS2664: Invalid module name in augmentation, module 'my-module' cannot be found. + export function foo(): void; + export const bar: string; + } + + // Ambient module with multiple attributes + declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { + ~~~~~~~~~~~~ +!!! error TS2664: Invalid module name in augmentation, module 'multi-attr' cannot be found. + export const value: number; + } + + // Ambient module without import attributes (should still work) + declare module "regular-module" { + ~~~~~~~~~~~~~~~~ +!!! error TS2664: Invalid module name in augmentation, module 'regular-module' cannot be found. + export function baz(): void; + } + + export {}; + + \ No newline at end of file diff --git a/tests/baselines/reference/ambientModuleWithImportAttributes.js b/tests/baselines/reference/ambientModuleWithImportAttributes.js new file mode 100644 index 0000000000000..9dbe1804483a3 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributes.js @@ -0,0 +1,58 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts] //// + +//// [ambientModuleWithImportAttributes.ts] +// Ambient module declaration with import attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +// Ambient module with specific name and import attributes +declare module "my-module" with { type: "custom" } { + export function foo(): void; + export const bar: string; +} + +// Ambient module with multiple attributes +declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { + export const value: number; +} + +// Ambient module without import attributes (should still work) +declare module "regular-module" { + export function baz(): void; +} + +export {}; + + + +//// [ambientModuleWithImportAttributes.js] +export {}; + + +//// [ambientModuleWithImportAttributes.d.ts] +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} +declare module "my-module" with { type: "custom" } { + function foo(): void; + const bar: string; +} +declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { + const value: number; +} +declare module "regular-module" { + function baz(): void; +} +export {}; diff --git a/tests/baselines/reference/ambientModuleWithImportAttributes.symbols b/tests/baselines/reference/ambientModuleWithImportAttributes.symbols new file mode 100644 index 0000000000000..423ce5b75f974 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributes.symbols @@ -0,0 +1,55 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts] //// + +=== ambientModuleWithImportAttributes.ts === +// Ambient module declaration with import attributes +declare module "*.css" with { type: "css" } { +>"*.css" : Symbol("*.css", Decl(ambientModuleWithImportAttributes.ts, 0, 0)) + + const stylesheet: CSSStyleSheet; +>stylesheet : Symbol(stylesheet, Decl(ambientModuleWithImportAttributes.ts, 2, 9)) +>CSSStyleSheet : Symbol(CSSStyleSheet, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + export default stylesheet; +>stylesheet : Symbol(stylesheet, Decl(ambientModuleWithImportAttributes.ts, 2, 9)) +} + +declare module "*.json" with { type: "json" } { +>"*.json" : Symbol("*.json", Decl(ambientModuleWithImportAttributes.ts, 4, 1)) + + const data: any; +>data : Symbol(data, Decl(ambientModuleWithImportAttributes.ts, 7, 9)) + + export default data; +>data : Symbol(data, Decl(ambientModuleWithImportAttributes.ts, 7, 9)) +} + +// Ambient module with specific name and import attributes +declare module "my-module" with { type: "custom" } { +>"my-module" : Symbol("my-module", Decl(ambientModuleWithImportAttributes.ts, 9, 1)) + + export function foo(): void; +>foo : Symbol(foo, Decl(ambientModuleWithImportAttributes.ts, 12, 52)) + + export const bar: string; +>bar : Symbol(bar, Decl(ambientModuleWithImportAttributes.ts, 14, 16)) +} + +// Ambient module with multiple attributes +declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { +>"multi-attr" : Symbol("multi-attr", Decl(ambientModuleWithImportAttributes.ts, 15, 1)) + + export const value: number; +>value : Symbol(value, Decl(ambientModuleWithImportAttributes.ts, 19, 16)) +} + +// Ambient module without import attributes (should still work) +declare module "regular-module" { +>"regular-module" : Symbol("regular-module", Decl(ambientModuleWithImportAttributes.ts, 20, 1)) + + export function baz(): void; +>baz : Symbol(baz, Decl(ambientModuleWithImportAttributes.ts, 23, 33)) +} + +export {}; + + diff --git a/tests/baselines/reference/ambientModuleWithImportAttributes.types b/tests/baselines/reference/ambientModuleWithImportAttributes.types new file mode 100644 index 0000000000000..e1f9572549beb --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributes.types @@ -0,0 +1,67 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts] //// + +=== ambientModuleWithImportAttributes.ts === +// Ambient module declaration with import attributes +declare module "*.css" with { type: "css" } { +>"*.css" : typeof import("*.css") +> : ^^^^^^^^^^^^^^^^^^^^^^ + + const stylesheet: CSSStyleSheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ + + export default stylesheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ +} + +declare module "*.json" with { type: "json" } { +>"*.json" : typeof import("*.json") +> : ^^^^^^^^^^^^^^^^^^^^^^^ + + const data: any; +>data : any +> : ^^^ + + export default data; +>data : any +> : ^^^ +} + +// Ambient module with specific name and import attributes +declare module "my-module" with { type: "custom" } { +>"my-module" : typeof import("my-module") +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + export function foo(): void; +>foo : () => void +> : ^^^^^^ + + export const bar: string; +>bar : string +> : ^^^^^^ +} + +// Ambient module with multiple attributes +declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { +>"multi-attr" : typeof import("multi-attr") +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + export const value: number; +>value : number +> : ^^^^^^ +} + +// Ambient module without import attributes (should still work) +declare module "regular-module" { +>"regular-module" : typeof import("regular-module") +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + export function baz(): void; +>baz : () => void +> : ^^^^^^ +} + +export {}; + + diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6ca73b0b2fe8b..db2ad471c1f3a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5477,6 +5477,7 @@ declare namespace ts { readonly modifiers?: NodeArray; readonly name: ModuleName; readonly body?: ModuleBody | JSDocNamespaceDeclaration; + readonly withClause?: ImportAttributes; } type NamespaceBody = ModuleBlock | NamespaceDeclaration; interface NamespaceDeclaration extends ModuleDeclaration { @@ -5552,7 +5553,7 @@ declare namespace ts { interface ImportAttributes extends Node { readonly token: SyntaxKind.WithKeyword | SyntaxKind.AssertKeyword; readonly kind: SyntaxKind.ImportAttributes; - readonly parent: ImportDeclaration | ExportDeclaration; + readonly parent: ImportDeclaration | ExportDeclaration | ModuleDeclaration; readonly elements: NodeArray; readonly multiLine?: boolean; } @@ -7732,8 +7733,8 @@ declare namespace ts { updateTypeAliasDeclaration(node: TypeAliasDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, typeParameters: readonly TypeParameterDeclaration[] | undefined, type: TypeNode): TypeAliasDeclaration; createEnumDeclaration(modifiers: readonly ModifierLike[] | undefined, name: string | Identifier, members: readonly EnumMember[]): EnumDeclaration; updateEnumDeclaration(node: EnumDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, members: readonly EnumMember[]): EnumDeclaration; - createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags?: NodeFlags): ModuleDeclaration; - updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined): ModuleDeclaration; + createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, flags?: NodeFlags, withClause?: ImportAttributes): ModuleDeclaration; + updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body: ModuleBody | undefined, withClause?: ImportAttributes): ModuleDeclaration; createModuleBlock(statements: readonly Statement[]): ModuleBlock; updateModuleBlock(node: ModuleBlock, statements: readonly Statement[]): ModuleBlock; createCaseBlock(clauses: readonly CaseOrDefaultClause[]): CaseBlock; diff --git a/tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts b/tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts new file mode 100644 index 0000000000000..66112c626c67b --- /dev/null +++ b/tests/cases/conformance/ambient/ambientModuleWithImportAttributes.ts @@ -0,0 +1,33 @@ +// @declaration: true +// @target: es2015 +// @module: esnext + +// Ambient module declaration with import attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +// Ambient module with specific name and import attributes +declare module "my-module" with { type: "custom" } { + export function foo(): void; + export const bar: string; +} + +// Ambient module with multiple attributes +declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { + export const value: number; +} + +// Ambient module without import attributes (should still work) +declare module "regular-module" { + export function baz(): void; +} + +export {}; + From e4f9c7cc4f974daa4e76ca797787fbf935dabd84 Mon Sep 17 00:00:00 2001 From: Jonathan Neal Date: Thu, 22 Jan 2026 01:04:45 -0500 Subject: [PATCH 2/4] Add semantic behavior for import attributes in ambient module declarations Implements module resolution and type checking to match imports with attributes to ambient module declarations with matching attributes. Changes: - Added importAttributesMatch() helper to compare import attributes - Added getImportAttributesFromLocation() to extract attributes from imports - Updated tryFindAmbientModule() to check attribute matching for exact matches - Updated resolveExternalModule() to check attribute matching for pattern matches - Added comprehensive test coverage for all matching scenarios Imports now properly resolve to ambient declarations only when attributes match: - import x from 'foo.css' with { type: 'css' } matches declare module '*.css' with { type: 'css' } - import x from 'foo.css' does NOT match declare module '*.css' with { type: 'css' } - Mismatched, missing, or extra attributes result in no match (type: any) --- src/compiler/checker.ts | 88 +++++++++- ...leWithImportAttributesSemantics.errors.txt | 86 +++++++++ ...ientModuleWithImportAttributesSemantics.js | 99 +++++++++++ ...oduleWithImportAttributesSemantics.symbols | 115 ++++++++++++ ...tModuleWithImportAttributesSemantics.types | 164 ++++++++++++++++++ ...ientModuleWithImportAttributesSemantics.ts | 63 +++++++ 6 files changed, 613 insertions(+), 2 deletions(-) create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesSemantics.js create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesSemantics.symbols create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types create mode 100644 tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 390c843b0c968..f5af7d2c85842 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4705,6 +4705,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { : undefined; } + function getImportAttributesFromLocation(location: Node): ImportAttributes | undefined { + // Try to find import attributes from the location node + const importDecl = findAncestor(location, isImportDeclaration); + if (importDecl?.attributes) { + return importDecl.attributes; + } + const exportDecl = findAncestor(location, isExportDeclaration); + if (exportDecl?.attributes) { + return exportDecl.attributes; + } + const importType = findAncestor(location, isImportTypeNode); + if (importType?.attributes) { + return importType.attributes; + } + return undefined; + } + function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node | undefined, isForAugmentation = false): Symbol | undefined { if (errorNode && startsWith(moduleReference, "@types/")) { const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1; @@ -4712,7 +4729,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { error(errorNode, diag, withoutAtTypePrefix, moduleReference); } - const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true); + // Get import attributes from the import/export statement + const importAttributes = getImportAttributesFromLocation(location); + + const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true, importAttributes); if (ambientModule) { return ambientModule; } @@ -4845,6 +4865,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (patternAmbientModules) { const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference); if (pattern) { + // Check if the pattern module has matching import attributes + const patternSymbol = pattern.symbol; + if (patternSymbol.declarations) { + const hasMatchingDeclaration = patternSymbol.declarations.some(decl => { + if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) { + return importAttributesMatch(importAttributes, decl.withClause); + } + return false; + }); + if (!hasMatchingDeclaration) { + // Pattern matched but attributes don't match - continue searching + return undefined; + } + } // If the module reference matched a pattern ambient module ('*.foo') but there's also a // module augmentation by the specific name requested ('a.foo'), we store the merged symbol // by the augmentation name ('a.foo'), because asking for *.foo should not give you exports @@ -16010,11 +16044,61 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return result; } - function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { + function importAttributesMatch(importAttributes: ImportAttributes | undefined, moduleAttributes: ImportAttributes | undefined): boolean { + // If neither has attributes, they match + if (!importAttributes && !moduleAttributes) { + return true; + } + // If only one has attributes, they don't match + if (!importAttributes || !moduleAttributes) { + return false; + } + // Both have attributes - check if they match + // For now, we require exact match of all attributes + if (importAttributes.elements.length !== moduleAttributes.elements.length) { + return false; + } + // Create a map of module attributes for easier lookup + const moduleAttrsMap = new Map(); + for (const attr of moduleAttributes.elements) { + const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text; + const value = isStringLiteral(attr.value) ? attr.value.text : undefined; + if (value !== undefined) { + moduleAttrsMap.set(name, value); + } + } + // Check that all import attributes match + for (const attr of importAttributes.elements) { + const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text; + const value = isStringLiteral(attr.value) ? attr.value.text : undefined; + if (value === undefined || moduleAttrsMap.get(name) !== value) { + return false; + } + } + return true; + } + + function tryFindAmbientModule(moduleName: string, withAugmentations: boolean, importAttributes?: ImportAttributes) { if (isExternalModuleNameRelative(moduleName)) { return undefined; } const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule); + if (!symbol) { + return undefined; + } + // Check if the module declaration has matching import attributes + const declarations = symbol.declarations; + if (declarations) { + const hasMatchingDeclaration = declarations.some(decl => { + if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) { + return importAttributesMatch(importAttributes, decl.withClause); + } + return false; + }); + if (!hasMatchingDeclaration) { + return undefined; + } + } // merged symbol is module declaration symbol combined with all augmentations return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol; } diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt new file mode 100644 index 0000000000000..4827fe05754b8 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt @@ -0,0 +1,86 @@ +test.ts(2,33): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(6,30): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(14,38): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(18,31): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(26,35): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(30,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(34,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + + +==== types.d.ts (0 errors) ==== + // Test exact match with attributes + declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; + } + + // Test exact match with different attributes + declare module "*.json" with { type: "json" } { + const data: any; + export default data; + } + + // Test exact match without attributes + declare module "*.txt" { + const content: string; + export default content; + } + + // Test multiple attributes + declare module "*.wasm" with { type: "module", version: "1" } { + const module: WebAssembly.Module; + export default module; + } + +==== test.ts (7 errors) ==== + // Should resolve correctly - matching attributes + import styles from "styles.css" with { type: "css" }; + ~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + styles; // Should be CSSStyleSheet + + // Should resolve correctly - matching attributes + import data from "data.json" with { type: "json" }; + ~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + data; // Should be any + + // Should resolve correctly - no attributes on either side + import text from "file.txt"; + text; // Should be string + + // Should resolve correctly - multiple matching attributes + import wasmModule from "module.wasm" with { type: "module", version: "1" }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + wasmModule; // Should be WebAssembly.Module + + // Should NOT resolve - import has attributes but declaration doesn't + import text2 from "file2.txt" with { type: "text" }; + ~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + text2; // Should be any (no match) + + // Should NOT resolve - import has no attributes but declaration does + import styles2 from "styles2.css"; + styles2; // Should be any (no match) + + // Should NOT resolve - mismatched attribute values + import styles3 from "styles3.css" with { type: "style" }; + ~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + styles3; // Should be any (no match) + + // Should NOT resolve - missing attribute + import wasmModule2 from "module2.wasm" with { type: "module" }; + ~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + wasmModule2; // Should be any (no match - missing version attribute) + + // Should NOT resolve - extra attribute + import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. + wasmModule3; // Should be any (no match - extra attribute) + + \ No newline at end of file diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.js b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.js new file mode 100644 index 0000000000000..6923770bce5d6 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.js @@ -0,0 +1,99 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts] //// + +//// [types.d.ts] +// Test exact match with attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +// Test exact match with different attributes +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +// Test exact match without attributes +declare module "*.txt" { + const content: string; + export default content; +} + +// Test multiple attributes +declare module "*.wasm" with { type: "module", version: "1" } { + const module: WebAssembly.Module; + export default module; +} + +//// [test.ts] +// Should resolve correctly - matching attributes +import styles from "styles.css" with { type: "css" }; +styles; // Should be CSSStyleSheet + +// Should resolve correctly - matching attributes +import data from "data.json" with { type: "json" }; +data; // Should be any + +// Should resolve correctly - no attributes on either side +import text from "file.txt"; +text; // Should be string + +// Should resolve correctly - multiple matching attributes +import wasmModule from "module.wasm" with { type: "module", version: "1" }; +wasmModule; // Should be WebAssembly.Module + +// Should NOT resolve - import has attributes but declaration doesn't +import text2 from "file2.txt" with { type: "text" }; +text2; // Should be any (no match) + +// Should NOT resolve - import has no attributes but declaration does +import styles2 from "styles2.css"; +styles2; // Should be any (no match) + +// Should NOT resolve - mismatched attribute values +import styles3 from "styles3.css" with { type: "style" }; +styles3; // Should be any (no match) + +// Should NOT resolve - missing attribute +import wasmModule2 from "module2.wasm" with { type: "module" }; +wasmModule2; // Should be any (no match - missing version attribute) + +// Should NOT resolve - extra attribute +import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; +wasmModule3; // Should be any (no match - extra attribute) + + + +//// [test.js] +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// Should resolve correctly - matching attributes +const styles_css_1 = __importDefault(require("styles.css")); +styles_css_1.default; // Should be CSSStyleSheet +// Should resolve correctly - matching attributes +const data_json_1 = __importDefault(require("data.json")); +data_json_1.default; // Should be any +// Should resolve correctly - no attributes on either side +const file_txt_1 = __importDefault(require("file.txt")); +file_txt_1.default; // Should be string +// Should resolve correctly - multiple matching attributes +const module_wasm_1 = __importDefault(require("module.wasm")); +module_wasm_1.default; // Should be WebAssembly.Module +// Should NOT resolve - import has attributes but declaration doesn't +const file2_txt_1 = __importDefault(require("file2.txt")); +file2_txt_1.default; // Should be any (no match) +// Should NOT resolve - import has no attributes but declaration does +const styles2_css_1 = __importDefault(require("styles2.css")); +styles2_css_1.default; // Should be any (no match) +// Should NOT resolve - mismatched attribute values +const styles3_css_1 = __importDefault(require("styles3.css")); +styles3_css_1.default; // Should be any (no match) +// Should NOT resolve - missing attribute +const module2_wasm_1 = __importDefault(require("module2.wasm")); +module2_wasm_1.default; // Should be any (no match - missing version attribute) +// Should NOT resolve - extra attribute +const module3_wasm_1 = __importDefault(require("module3.wasm")); +module3_wasm_1.default; // Should be any (no match - extra attribute) diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.symbols b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.symbols new file mode 100644 index 0000000000000..6101cbbb22065 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.symbols @@ -0,0 +1,115 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts] //// + +=== types.d.ts === +// Test exact match with attributes +declare module "*.css" with { type: "css" } { +>"*.css" : Symbol("*.css", Decl(types.d.ts, 0, 0)) + + const stylesheet: CSSStyleSheet; +>stylesheet : Symbol(stylesheet, Decl(types.d.ts, 2, 9)) +>CSSStyleSheet : Symbol(CSSStyleSheet, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + export default stylesheet; +>stylesheet : Symbol(stylesheet, Decl(types.d.ts, 2, 9)) +} + +// Test exact match with different attributes +declare module "*.json" with { type: "json" } { +>"*.json" : Symbol("*.json", Decl(types.d.ts, 4, 1)) + + const data: any; +>data : Symbol(data, Decl(types.d.ts, 8, 9)) + + export default data; +>data : Symbol(data, Decl(types.d.ts, 8, 9)) +} + +// Test exact match without attributes +declare module "*.txt" { +>"*.txt" : Symbol("*.txt", Decl(types.d.ts, 10, 1)) + + const content: string; +>content : Symbol(content, Decl(types.d.ts, 14, 9)) + + export default content; +>content : Symbol(content, Decl(types.d.ts, 14, 9)) +} + +// Test multiple attributes +declare module "*.wasm" with { type: "module", version: "1" } { +>"*.wasm" : Symbol("*.wasm", Decl(types.d.ts, 16, 1)) + + const module: WebAssembly.Module; +>module : Symbol(module, Decl(types.d.ts, 20, 9)) +>WebAssembly : Symbol(WebAssembly, Decl(lib.dom.d.ts, --, --)) +>Module : Symbol(WebAssembly.Module, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + export default module; +>module : Symbol(module, Decl(types.d.ts, 20, 9)) +} + +=== test.ts === +// Should resolve correctly - matching attributes +import styles from "styles.css" with { type: "css" }; +>styles : Symbol(styles, Decl(test.ts, 1, 6)) + +styles; // Should be CSSStyleSheet +>styles : Symbol(styles, Decl(test.ts, 1, 6)) + +// Should resolve correctly - matching attributes +import data from "data.json" with { type: "json" }; +>data : Symbol(data, Decl(test.ts, 5, 6)) + +data; // Should be any +>data : Symbol(data, Decl(test.ts, 5, 6)) + +// Should resolve correctly - no attributes on either side +import text from "file.txt"; +>text : Symbol(text, Decl(test.ts, 9, 6)) + +text; // Should be string +>text : Symbol(text, Decl(test.ts, 9, 6)) + +// Should resolve correctly - multiple matching attributes +import wasmModule from "module.wasm" with { type: "module", version: "1" }; +>wasmModule : Symbol(wasmModule, Decl(test.ts, 13, 6)) + +wasmModule; // Should be WebAssembly.Module +>wasmModule : Symbol(wasmModule, Decl(test.ts, 13, 6)) + +// Should NOT resolve - import has attributes but declaration doesn't +import text2 from "file2.txt" with { type: "text" }; +>text2 : Symbol(text2, Decl(test.ts, 17, 6)) + +text2; // Should be any (no match) +>text2 : Symbol(text2, Decl(test.ts, 17, 6)) + +// Should NOT resolve - import has no attributes but declaration does +import styles2 from "styles2.css"; +>styles2 : Symbol(styles2, Decl(test.ts, 21, 6)) + +styles2; // Should be any (no match) +>styles2 : Symbol(styles2, Decl(test.ts, 21, 6)) + +// Should NOT resolve - mismatched attribute values +import styles3 from "styles3.css" with { type: "style" }; +>styles3 : Symbol(styles3, Decl(test.ts, 25, 6)) + +styles3; // Should be any (no match) +>styles3 : Symbol(styles3, Decl(test.ts, 25, 6)) + +// Should NOT resolve - missing attribute +import wasmModule2 from "module2.wasm" with { type: "module" }; +>wasmModule2 : Symbol(wasmModule2, Decl(test.ts, 29, 6)) + +wasmModule2; // Should be any (no match - missing version attribute) +>wasmModule2 : Symbol(wasmModule2, Decl(test.ts, 29, 6)) + +// Should NOT resolve - extra attribute +import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; +>wasmModule3 : Symbol(wasmModule3, Decl(test.ts, 33, 6)) + +wasmModule3; // Should be any (no match - extra attribute) +>wasmModule3 : Symbol(wasmModule3, Decl(test.ts, 33, 6)) + + diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types new file mode 100644 index 0000000000000..885ecd2b627ec --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types @@ -0,0 +1,164 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts] //// + +=== types.d.ts === +// Test exact match with attributes +declare module "*.css" with { type: "css" } { +>"*.css" : typeof import("*.css") +> : ^^^^^^^^^^^^^^^^^^^^^^ + + const stylesheet: CSSStyleSheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ + + export default stylesheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ +} + +// Test exact match with different attributes +declare module "*.json" with { type: "json" } { +>"*.json" : typeof import("*.json") +> : ^^^^^^^^^^^^^^^^^^^^^^^ + + const data: any; +>data : any +> : ^^^ + + export default data; +>data : any +> : ^^^ +} + +// Test exact match without attributes +declare module "*.txt" { +>"*.txt" : typeof import("*.txt") +> : ^^^^^^^^^^^^^^^^^^^^^^ + + const content: string; +>content : string +> : ^^^^^^ + + export default content; +>content : string +> : ^^^^^^ +} + +// Test multiple attributes +declare module "*.wasm" with { type: "module", version: "1" } { +>"*.wasm" : typeof import("*.wasm") +> : ^^^^^^^^^^^^^^^^^^^^^^^ + + const module: WebAssembly.Module; +>module : WebAssembly.Module +> : ^^^^^^^^^^^^^^^^^^ +>WebAssembly : any +> : ^^^ + + export default module; +>module : WebAssembly.Module +> : ^^^^^^^^^^^^^^^^^^ +} + +=== test.ts === +// Should resolve correctly - matching attributes +import styles from "styles.css" with { type: "css" }; +>styles : CSSStyleSheet +> : ^^^^^^^^^^^^^ +>type : any +> : ^^^ + +styles; // Should be CSSStyleSheet +>styles : CSSStyleSheet +> : ^^^^^^^^^^^^^ + +// Should resolve correctly - matching attributes +import data from "data.json" with { type: "json" }; +>data : any +> : ^^^ +>type : any +> : ^^^ + +data; // Should be any +>data : any +> : ^^^ + +// Should resolve correctly - no attributes on either side +import text from "file.txt"; +>text : string +> : ^^^^^^ + +text; // Should be string +>text : string +> : ^^^^^^ + +// Should resolve correctly - multiple matching attributes +import wasmModule from "module.wasm" with { type: "module", version: "1" }; +>wasmModule : WebAssembly.Module +> : ^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ +>version : any +> : ^^^ + +wasmModule; // Should be WebAssembly.Module +>wasmModule : WebAssembly.Module +> : ^^^^^^^^^^^^^^^^^^ + +// Should NOT resolve - import has attributes but declaration doesn't +import text2 from "file2.txt" with { type: "text" }; +>text2 : any +> : ^^^ +>type : any +> : ^^^ + +text2; // Should be any (no match) +>text2 : any +> : ^^^ + +// Should NOT resolve - import has no attributes but declaration does +import styles2 from "styles2.css"; +>styles2 : any +> : ^^^ + +styles2; // Should be any (no match) +>styles2 : any +> : ^^^ + +// Should NOT resolve - mismatched attribute values +import styles3 from "styles3.css" with { type: "style" }; +>styles3 : any +> : ^^^ +>type : any +> : ^^^ + +styles3; // Should be any (no match) +>styles3 : any +> : ^^^ + +// Should NOT resolve - missing attribute +import wasmModule2 from "module2.wasm" with { type: "module" }; +>wasmModule2 : any +> : ^^^ +>type : any +> : ^^^ + +wasmModule2; // Should be any (no match - missing version attribute) +>wasmModule2 : any +> : ^^^ + +// Should NOT resolve - extra attribute +import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; +>wasmModule3 : any +> : ^^^ +>type : any +> : ^^^ +>version : any +> : ^^^ +>extra : any +> : ^^^ + +wasmModule3; // Should be any (no match - extra attribute) +>wasmModule3 : any +> : ^^^ + + diff --git a/tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts b/tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts new file mode 100644 index 0000000000000..902b9302a9690 --- /dev/null +++ b/tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts @@ -0,0 +1,63 @@ +// @module: nodenext +// @filename: types.d.ts +// Test exact match with attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +// Test exact match with different attributes +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +// Test exact match without attributes +declare module "*.txt" { + const content: string; + export default content; +} + +// Test multiple attributes +declare module "*.wasm" with { type: "module", version: "1" } { + const module: WebAssembly.Module; + export default module; +} + +// @filename: test.ts +// Should resolve correctly - matching attributes +import styles from "styles.css" with { type: "css" }; +styles; // Should be CSSStyleSheet + +// Should resolve correctly - matching attributes +import data from "data.json" with { type: "json" }; +data; // Should be any + +// Should resolve correctly - no attributes on either side +import text from "file.txt"; +text; // Should be string + +// Should resolve correctly - multiple matching attributes +import wasmModule from "module.wasm" with { type: "module", version: "1" }; +wasmModule; // Should be WebAssembly.Module + +// Should NOT resolve - import has attributes but declaration doesn't +import text2 from "file2.txt" with { type: "text" }; +text2; // Should be any (no match) + +// Should NOT resolve - import has no attributes but declaration does +import styles2 from "styles2.css"; +styles2; // Should be any (no match) + +// Should NOT resolve - mismatched attribute values +import styles3 from "styles3.css" with { type: "style" }; +styles3; // Should be any (no match) + +// Should NOT resolve - missing attribute +import wasmModule2 from "module2.wasm" with { type: "module" }; +wasmModule2; // Should be any (no match - missing version attribute) + +// Should NOT resolve - extra attribute +import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; +wasmModule3; // Should be any (no match - extra attribute) + From edc1a1f0d1910e0993aeb8e3e465aadc8aa3a274 Mon Sep 17 00:00:00 2001 From: Jonathan Neal Date: Thu, 22 Jan 2026 01:12:33 -0500 Subject: [PATCH 3/4] Add validation and diagnostics for import attribute mismatches Implements error reporting when import attributes don't match between import statements and ambient module declarations. Changes: - Added diagnostic messages TS18062 and TS18063 for attribute mismatches - Updated resolveExternalModule to report TS18063 when pattern matches but attributes don't match - Created comprehensive test cases for diagnostic error messages The diagnostic TS18063 is reported when: - Pattern matches but attribute values are different - Pattern matches but import has no attributes when declaration requires them - Pattern matches but import has attributes when declaration doesn't - Pattern matches but attributes have different keys or values --- src/compiler/checker.ts | 5 +- src/compiler/diagnosticMessages.json | 8 +++ ...WithImportAttributesDiagnostics.errors.txt | 45 ++++++++++++ ...ntModuleWithImportAttributesDiagnostics.js | 40 +++++++++++ ...uleWithImportAttributesDiagnostics.symbols | 53 ++++++++++++++ ...oduleWithImportAttributesDiagnostics.types | 71 +++++++++++++++++++ ...leWithImportAttributesSemantics.errors.txt | 17 ++++- ...ntModuleWithImportAttributesDiagnostics.ts | 34 +++++++++ 8 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.errors.txt create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.js create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.symbols create mode 100644 tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types create mode 100644 tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f5af7d2c85842..aa0e7dafed03f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4875,7 +4875,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return false; }); if (!hasMatchingDeclaration) { - // Pattern matched but attributes don't match - continue searching + // Pattern matched but attributes don't match + if (errorNode && moduleNotFoundError) { + error(errorNode, Diagnostics.No_ambient_module_declaration_matches_import_of_0_with_the_specified_import_attributes, moduleReference); + } return undefined; } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 1a69dba02a6c1..3fa7f79ac0743 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -8547,5 +8547,13 @@ "'{0}' is not a valid meta-property for keyword 'import'. Did you mean 'meta' or 'defer'?": { "category": "Error", "code": 18061 + }, + "Module '{0}' was resolved to '{1}', but import attributes do not match the ambient module declaration.": { + "category": "Error", + "code": 18062 + }, + "No ambient module declaration matches import of '{0}' with the specified import attributes.": { + "category": "Error", + "code": 18063 } } diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.errors.txt b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.errors.txt new file mode 100644 index 0000000000000..918b0fec0e9ec --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.errors.txt @@ -0,0 +1,45 @@ +test.ts(2,21): error TS18063: No ambient module declaration matches import of 'styles.css' with the specified import attributes. +test.ts(5,21): error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes. +test.ts(8,18): error TS18063: No ambient module declaration matches import of 'file.txt' with the specified import attributes. +test.ts(11,18): error TS18063: No ambient module declaration matches import of 'data.json' with the specified import attributes. + + +==== types.d.ts (0 errors) ==== + // Declare ambient modules with import attributes + declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; + } + + declare module "*.json" with { type: "json" } { + const data: any; + export default data; + } + + declare module "*.txt" { + const content: string; + export default content; + } + +==== test.ts (4 errors) ==== + // Should error - pattern matches but attributes don't match + import styles1 from "styles.css" with { type: "style" }; + ~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'styles.css' with the specified import attributes. + + // Should error - pattern matches but import has no attributes when declaration requires them + import styles2 from "styles2.css"; + ~~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes. + + // Should error - pattern matches but import has attributes when declaration doesn't + import text from "file.txt" with { type: "text" }; + ~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'file.txt' with the specified import attributes. + + // Should error - pattern matches but attribute value is wrong + import data from "data.json" with { type: "data" }; + ~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'data.json' with the specified import attributes. + + \ No newline at end of file diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.js b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.js new file mode 100644 index 0000000000000..b64c23ed2e09f --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.js @@ -0,0 +1,40 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts] //// + +//// [types.d.ts] +// Declare ambient modules with import attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +declare module "*.txt" { + const content: string; + export default content; +} + +//// [test.ts] +// Should error - pattern matches but attributes don't match +import styles1 from "styles.css" with { type: "style" }; + +// Should error - pattern matches but import has no attributes when declaration requires them +import styles2 from "styles2.css"; + +// Should error - pattern matches but import has attributes when declaration doesn't +import text from "file.txt" with { type: "text" }; + +// Should error - pattern matches but attribute value is wrong +import data from "data.json" with { type: "data" }; + + + +//// [test.js] +export {}; + + +//// [test.d.ts] +export {}; diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.symbols b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.symbols new file mode 100644 index 0000000000000..016a91f5cb800 --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.symbols @@ -0,0 +1,53 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts] //// + +=== types.d.ts === +// Declare ambient modules with import attributes +declare module "*.css" with { type: "css" } { +>"*.css" : Symbol("*.css", Decl(types.d.ts, 0, 0)) + + const stylesheet: CSSStyleSheet; +>stylesheet : Symbol(stylesheet, Decl(types.d.ts, 2, 9)) +>CSSStyleSheet : Symbol(CSSStyleSheet, Decl(lib.dom.d.ts, --, --), Decl(lib.dom.d.ts, --, --)) + + export default stylesheet; +>stylesheet : Symbol(stylesheet, Decl(types.d.ts, 2, 9)) +} + +declare module "*.json" with { type: "json" } { +>"*.json" : Symbol("*.json", Decl(types.d.ts, 4, 1)) + + const data: any; +>data : Symbol(data, Decl(types.d.ts, 7, 9)) + + export default data; +>data : Symbol(data, Decl(types.d.ts, 7, 9)) +} + +declare module "*.txt" { +>"*.txt" : Symbol("*.txt", Decl(types.d.ts, 9, 1)) + + const content: string; +>content : Symbol(content, Decl(types.d.ts, 12, 9)) + + export default content; +>content : Symbol(content, Decl(types.d.ts, 12, 9)) +} + +=== test.ts === +// Should error - pattern matches but attributes don't match +import styles1 from "styles.css" with { type: "style" }; +>styles1 : Symbol(styles1, Decl(test.ts, 1, 6)) + +// Should error - pattern matches but import has no attributes when declaration requires them +import styles2 from "styles2.css"; +>styles2 : Symbol(styles2, Decl(test.ts, 4, 6)) + +// Should error - pattern matches but import has attributes when declaration doesn't +import text from "file.txt" with { type: "text" }; +>text : Symbol(text, Decl(test.ts, 7, 6)) + +// Should error - pattern matches but attribute value is wrong +import data from "data.json" with { type: "data" }; +>data : Symbol(data, Decl(test.ts, 10, 6)) + + diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types new file mode 100644 index 0000000000000..e5dc9cf7ab21f --- /dev/null +++ b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types @@ -0,0 +1,71 @@ +//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts] //// + +=== types.d.ts === +// Declare ambient modules with import attributes +declare module "*.css" with { type: "css" } { +>"*.css" : typeof import("*.css") +> : ^^^^^^^^^^^^^^^^^^^^^^ + + const stylesheet: CSSStyleSheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ + + export default stylesheet; +>stylesheet : CSSStyleSheet +> : ^^^^^^^^^^^^^ +} + +declare module "*.json" with { type: "json" } { +>"*.json" : typeof import("*.json") +> : ^^^^^^^^^^^^^^^^^^^^^^^ + + const data: any; +>data : any +> : ^^^ + + export default data; +>data : any +> : ^^^ +} + +declare module "*.txt" { +>"*.txt" : typeof import("*.txt") +> : ^^^^^^^^^^^^^^^^^^^^^^ + + const content: string; +>content : string +> : ^^^^^^ + + export default content; +>content : string +> : ^^^^^^ +} + +=== test.ts === +// Should error - pattern matches but attributes don't match +import styles1 from "styles.css" with { type: "style" }; +>styles1 : any +> : ^^^ +>type : any +> : ^^^ + +// Should error - pattern matches but import has no attributes when declaration requires them +import styles2 from "styles2.css"; +>styles2 : any +> : ^^^ + +// Should error - pattern matches but import has attributes when declaration doesn't +import text from "file.txt" with { type: "text" }; +>text : any +> : ^^^ +>type : any +> : ^^^ + +// Should error - pattern matches but attribute value is wrong +import data from "data.json" with { type: "data" }; +>data : any +> : ^^^ +>type : any +> : ^^^ + + diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt index 4827fe05754b8..a215d11f6d973 100644 --- a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.errors.txt @@ -1,9 +1,14 @@ test.ts(2,33): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. test.ts(6,30): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. test.ts(14,38): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(18,19): error TS18063: No ambient module declaration matches import of 'file2.txt' with the specified import attributes. test.ts(18,31): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(22,21): error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes. +test.ts(26,21): error TS18063: No ambient module declaration matches import of 'styles3.css' with the specified import attributes. test.ts(26,35): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(30,25): error TS18063: No ambient module declaration matches import of 'module2.wasm' with the specified import attributes. test.ts(30,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. +test.ts(34,25): error TS18063: No ambient module declaration matches import of 'module3.wasm' with the specified import attributes. test.ts(34,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. @@ -32,7 +37,7 @@ test.ts(34,40): error TS2856: Import attributes are not allowed on statements th export default module; } -==== test.ts (7 errors) ==== +==== test.ts (12 errors) ==== // Should resolve correctly - matching attributes import styles from "styles.css" with { type: "css" }; ~~~~~~~~~~~~~~~~~~~~ @@ -57,28 +62,38 @@ test.ts(34,40): error TS2856: Import attributes are not allowed on statements th // Should NOT resolve - import has attributes but declaration doesn't import text2 from "file2.txt" with { type: "text" }; + ~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'file2.txt' with the specified import attributes. ~~~~~~~~~~~~~~~~~~~~~ !!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. text2; // Should be any (no match) // Should NOT resolve - import has no attributes but declaration does import styles2 from "styles2.css"; + ~~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'styles2.css' with the specified import attributes. styles2; // Should be any (no match) // Should NOT resolve - mismatched attribute values import styles3 from "styles3.css" with { type: "style" }; + ~~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'styles3.css' with the specified import attributes. ~~~~~~~~~~~~~~~~~~~~~~ !!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. styles3; // Should be any (no match) // Should NOT resolve - missing attribute import wasmModule2 from "module2.wasm" with { type: "module" }; + ~~~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'module2.wasm' with the specified import attributes. ~~~~~~~~~~~~~~~~~~~~~~~ !!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. wasmModule2; // Should be any (no match - missing version attribute) // Should NOT resolve - extra attribute import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" }; + ~~~~~~~~~~~~~~ +!!! error TS18063: No ambient module declaration matches import of 'module3.wasm' with the specified import attributes. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ !!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls. wasmModule3; // Should be any (no match - extra attribute) diff --git a/tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts b/tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts new file mode 100644 index 0000000000000..5b8a903187f5a --- /dev/null +++ b/tests/cases/conformance/ambient/ambientModuleWithImportAttributesDiagnostics.ts @@ -0,0 +1,34 @@ +// @declaration: true +// @target: es2015 +// @module: esnext + +// @filename: types.d.ts +// Declare ambient modules with import attributes +declare module "*.css" with { type: "css" } { + const stylesheet: CSSStyleSheet; + export default stylesheet; +} + +declare module "*.json" with { type: "json" } { + const data: any; + export default data; +} + +declare module "*.txt" { + const content: string; + export default content; +} + +// @filename: test.ts +// Should error - pattern matches but attributes don't match +import styles1 from "styles.css" with { type: "style" }; + +// Should error - pattern matches but import has no attributes when declaration requires them +import styles2 from "styles2.css"; + +// Should error - pattern matches but import has attributes when declaration doesn't +import text from "file.txt" with { type: "text" }; + +// Should error - pattern matches but attribute value is wrong +import data from "data.json" with { type: "data" }; + From d020329868d5de18daf09ad4da539d21f0b41c74 Mon Sep 17 00:00:00 2001 From: Jonathan Neal Date: Thu, 22 Jan 2026 08:12:47 -0500 Subject: [PATCH 4/4] Fix forEachChild to include withClause for ModuleDeclaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test harness validates that all child nodes are visited by forEachChild in the correct order (by position in the source code). We added withClause to ModuleDeclaration but: 1. Forgot to update the forEachChild visitor in parser.ts to include it 2. Had it in the wrong order - withClause comes BEFORE body in the syntax The syntax is: declare module "name" with { ... } { ... } So the order is: modifiers → name → withClause → body This commit adds withClause to forEachChild in the correct position. --- src/compiler/parser.ts | 1 + .../reference/ambientModuleWithImportAttributes.types | 10 ++++++++++ .../ambientModuleWithImportAttributesDiagnostics.types | 4 ++++ .../ambientModuleWithImportAttributesSemantics.types | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index e782115a1e5d6..2ee6d8e3a3aec 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -923,6 +923,7 @@ const forEachChildTable: ForEachChildTable = { [SyntaxKind.ModuleDeclaration]: function forEachChildInModuleDeclaration(node: ModuleDeclaration, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { return visitNodes(cbNode, cbNodes, node.modifiers) || visitNode(cbNode, node.name) || + visitNode(cbNode, node.withClause) || visitNode(cbNode, node.body); }, [SyntaxKind.ImportEqualsDeclaration]: function forEachChildInImportEqualsDeclaration(node: ImportEqualsDeclaration, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray) => T | undefined): T | undefined { diff --git a/tests/baselines/reference/ambientModuleWithImportAttributes.types b/tests/baselines/reference/ambientModuleWithImportAttributes.types index e1f9572549beb..7b1c0328fa6be 100644 --- a/tests/baselines/reference/ambientModuleWithImportAttributes.types +++ b/tests/baselines/reference/ambientModuleWithImportAttributes.types @@ -5,6 +5,8 @@ declare module "*.css" with { type: "css" } { >"*.css" : typeof import("*.css") > : ^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const stylesheet: CSSStyleSheet; >stylesheet : CSSStyleSheet @@ -18,6 +20,8 @@ declare module "*.css" with { type: "css" } { declare module "*.json" with { type: "json" } { >"*.json" : typeof import("*.json") > : ^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const data: any; >data : any @@ -32,6 +36,8 @@ declare module "*.json" with { type: "json" } { declare module "my-module" with { type: "custom" } { >"my-module" : typeof import("my-module") > : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ export function foo(): void; >foo : () => void @@ -46,6 +52,10 @@ declare module "my-module" with { type: "custom" } { declare module "multi-attr" with { type: "json", integrity: "sha384-..." } { >"multi-attr" : typeof import("multi-attr") > : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ +>integrity : any +> : ^^^ export const value: number; >value : number diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types index e5dc9cf7ab21f..22610341e3529 100644 --- a/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types +++ b/tests/baselines/reference/ambientModuleWithImportAttributesDiagnostics.types @@ -5,6 +5,8 @@ declare module "*.css" with { type: "css" } { >"*.css" : typeof import("*.css") > : ^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const stylesheet: CSSStyleSheet; >stylesheet : CSSStyleSheet @@ -18,6 +20,8 @@ declare module "*.css" with { type: "css" } { declare module "*.json" with { type: "json" } { >"*.json" : typeof import("*.json") > : ^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const data: any; >data : any diff --git a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types index 885ecd2b627ec..b4244ba1c92e3 100644 --- a/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types +++ b/tests/baselines/reference/ambientModuleWithImportAttributesSemantics.types @@ -5,6 +5,8 @@ declare module "*.css" with { type: "css" } { >"*.css" : typeof import("*.css") > : ^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const stylesheet: CSSStyleSheet; >stylesheet : CSSStyleSheet @@ -19,6 +21,8 @@ declare module "*.css" with { type: "css" } { declare module "*.json" with { type: "json" } { >"*.json" : typeof import("*.json") > : ^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ const data: any; >data : any @@ -47,6 +51,10 @@ declare module "*.txt" { declare module "*.wasm" with { type: "module", version: "1" } { >"*.wasm" : typeof import("*.wasm") > : ^^^^^^^^^^^^^^^^^^^^^^^ +>type : any +> : ^^^ +>version : any +> : ^^^ const module: WebAssembly.Module; >module : WebAssembly.Module