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
135 changes: 123 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,7 @@ import {
tryGetJSDocSatisfiesTypeNode,
tryGetModuleSpecifierFromDeclaration,
tryGetPropertyAccessOrIdentifierToString,
tryGetTextOfPropertyName,
TryStatement,
TupleType,
TupleTypeNode,
Expand Down Expand Up @@ -4705,14 +4706,71 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
: undefined;
}

function getImportAttributesFromLocation(location: Node): ImportAttributes | undefined {
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;
}
const importCall = findAncestor(location, isImportCall);
if (importCall) {
return getImportAttributesFromImportCall(importCall);
}
return undefined;
}

function getImportAttributesFromImportCall(importCall: ImportCall): ImportAttributes | undefined {
// import("./module", { with: { type: "json" } })
if (importCall.arguments.length < 2) {
return undefined;
}
const optionsArg = importCall.arguments[1];
if (!isObjectLiteralExpression(optionsArg)) {
return undefined;
}
for (const prop of optionsArg.properties) {
if (isPropertyAssignment(prop) && isObjectLiteralExpression(prop.initializer)) {
const propName = tryGetTextOfPropertyName(prop.name);
if (propName === "with" as __String || propName === "assert" as __String) {
return createSyntheticImportAttributes(prop.initializer);
}
}
}
return undefined;
}

function createSyntheticImportAttributes(objectLiteral: ObjectLiteralExpression): ImportAttributes | undefined {
// Create synthetic ImportAttribute elements from PropertyAssignment nodes
// PropertyAssignment has { name, initializer } but ImportAttribute needs { name, value }
const elements: { name: PropertyName; value: Expression; }[] = [];
for (const prop of objectLiteral.properties) {
if (isPropertyAssignment(prop) && isStringLiteral(prop.initializer)) {
elements.push({ name: prop.name, value: prop.initializer });
}
}
if (elements.length === 0) {
return undefined;
}
return { elements: elements as unknown as NodeArray<ImportAttribute> } as unknown as ImportAttributes;
}

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);
const importAttributes = getImportAttributesFromLocation(location);

const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true, importAttributes);
if (ambientModule) {
return ambientModule;
}
Expand Down Expand Up @@ -4800,6 +4858,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

if (sourceFile.symbol) {
// Check for pattern ambient module with matching import attributes
if (importAttributes?.elements.length) {
const patternMatch = tryFindPatternAmbientModuleWithAttributes(moduleReference, importAttributes);
if (patternMatch) {
return patternMatch;
}
}

if (errorNode && resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
errorOnImplicitAnyModule(/*isError*/ false, errorNode, currentSourceFile, mode, resolvedModule, moduleReference);
}
Expand Down Expand Up @@ -4845,15 +4911,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (patternAmbientModules) {
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference);
if (pattern) {
// 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
// from a.foo.
const augmentation = patternAmbientModuleAugmentations && patternAmbientModuleAugmentations.get(moduleReference);
if (augmentation) {
return getMergedSymbol(augmentation);
if (!hasMatchingImportAttributes(pattern.symbol, importAttributes)) {
if (errorNode && moduleNotFoundError) {
error(errorNode, Diagnostics.No_ambient_module_declaration_matches_import_of_0_with_the_specified_import_attributes, moduleReference);
}
return undefined;
}
return getMergedSymbol(pattern.symbol);
const augmentation = patternAmbientModuleAugmentations?.get(moduleReference);
return getMergedSymbol(augmentation || pattern.symbol);
}
}

Expand Down Expand Up @@ -16010,13 +16075,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}

function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) {
function importAttributesMatch(importAttributes: ImportAttributes | undefined, moduleAttributes: ImportAttributes | undefined): boolean {
if (!importAttributes && !moduleAttributes) {
return true;
}
if (!importAttributes || !moduleAttributes) {
return false;
}
if (importAttributes.elements.length !== moduleAttributes.elements.length) {
return false;
}
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);
}
}
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 hasMatchingImportAttributes(symbol: Symbol, importAttributes: ImportAttributes | undefined): boolean {
return !symbol.declarations || symbol.declarations.some(decl => isModuleDeclaration(decl) && isStringLiteral(decl.name) && importAttributesMatch(importAttributes, decl.withClause));
}

function tryFindAmbientModule(moduleName: string, withAugmentations: boolean, importAttributes?: ImportAttributes) {
if (isExternalModuleNameRelative(moduleName)) {
return undefined;
}
const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule);
// merged symbol is module declaration symbol combined with all augmentations
return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol;
if (!symbol || !hasMatchingImportAttributes(symbol, importAttributes)) {
return undefined;
}
return withAugmentations ? getMergedSymbol(symbol) : symbol;
}

function tryFindPatternAmbientModuleWithAttributes(moduleReference: string, importAttributes: ImportAttributes | undefined): Symbol | undefined {
if (!patternAmbientModules) {
return undefined;
}
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference);
if (!pattern || !hasMatchingImportAttributes(pattern.symbol, importAttributes)) {
return undefined;
}
const augmentation = patternAmbientModuleAugmentations?.get(moduleReference);
return getMergedSymbol(augmentation || pattern.symbol);
}

function hasEffectiveQuestionToken(node: ParameterDeclaration | JSDocParameterTag | JSDocPropertyTag) {
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
Loading