Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 89 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4705,14 +4705,34 @@ 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;
const withoutAtTypePrefix = removePrefix(moduleReference, "@types/");
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;
}
Expand Down Expand Up @@ -4845,6 +4865,23 @@ 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
if (errorNode && moduleNotFoundError) {
error(errorNode, Diagnostics.No_ambient_module_declaration_matches_import_of_0_with_the_specified_import_attributes, moduleReference);
}
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
Expand Down Expand Up @@ -16010,11 +16047,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<string, string>();
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;
}
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
5 changes: 5 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
8 changes: 6 additions & 2 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4546,12 +4546,14 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
name: ModuleName,
body: ModuleBody | undefined,
flags = NodeFlags.None,
withClause?: ImportAttributes,
) {
const node = createBaseDeclaration<ModuleDeclaration>(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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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) :
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ const forEachChildTable: ForEachChildTable = {
[SyntaxKind.ModuleDeclaration]: function forEachChildInModuleDeclaration<T>(node: ModuleDeclaration, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => 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<T>(node: ImportEqualsDeclaration, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
Expand Down Expand Up @@ -8324,14 +8325,15 @@ namespace Parser {
name = parseLiteralNode() as StringLiteral;
name.text = internIdentifier(name.text);
}
const withClause = tryParseImportAttributes();
let body: ModuleBlock | undefined;
if (token() === SyntaxKind.OpenBraceToken) {
body = parseModuleBlock();
}
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);
}

Expand Down
4 changes: 3 additions & 1 deletion src/compiler/transformers/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -1371,6 +1371,7 @@ export function transformDeclarations(context: TransformationContext): Transform
updated.name,
updated.body,
updated.flags | NodeFlags.Namespace,
updated.withClause,
);

setOriginalNode(fixed, updated);
Expand Down Expand Up @@ -1512,6 +1513,7 @@ export function transformDeclarations(context: TransformationContext): Transform
modifiers,
namespaceDecl.name,
namespaceDecl.body,
namespaceDecl.withClause,
);

const exportDefaultDeclaration = factory.createExportAssignment(
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3634,6 +3634,7 @@ export interface ModuleDeclaration extends DeclarationStatement, JSDocContainer,
readonly modifiers?: NodeArray<ModifierLike>;
readonly name: ModuleName;
readonly body?: ModuleBody | JSDocNamespaceDeclaration;
readonly withClause?: ImportAttributes;
}

export type NamespaceBody =
Expand Down Expand Up @@ -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<ImportAttribute>;
readonly multiLine?: boolean;
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/compiler/visitorPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
},

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {};


58 changes: 58 additions & 0 deletions tests/baselines/reference/ambientModuleWithImportAttributes.js
Original file line number Diff line number Diff line change
@@ -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 {};
Loading