Skip to content

Commit e4f9c7c

Browse files
committed
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)
1 parent f23f1c6 commit e4f9c7c

6 files changed

Lines changed: 613 additions & 2 deletions

src/compiler/checker.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4705,14 +4705,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47054705
: undefined;
47064706
}
47074707

4708+
function getImportAttributesFromLocation(location: Node): ImportAttributes | undefined {
4709+
// Try to find import attributes from the location node
4710+
const importDecl = findAncestor(location, isImportDeclaration);
4711+
if (importDecl?.attributes) {
4712+
return importDecl.attributes;
4713+
}
4714+
const exportDecl = findAncestor(location, isExportDeclaration);
4715+
if (exportDecl?.attributes) {
4716+
return exportDecl.attributes;
4717+
}
4718+
const importType = findAncestor(location, isImportTypeNode);
4719+
if (importType?.attributes) {
4720+
return importType.attributes;
4721+
}
4722+
return undefined;
4723+
}
4724+
47084725
function resolveExternalModule(location: Node, moduleReference: string, moduleNotFoundError: DiagnosticMessage | undefined, errorNode: Node | undefined, isForAugmentation = false): Symbol | undefined {
47094726
if (errorNode && startsWith(moduleReference, "@types/")) {
47104727
const diag = Diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1;
47114728
const withoutAtTypePrefix = removePrefix(moduleReference, "@types/");
47124729
error(errorNode, diag, withoutAtTypePrefix, moduleReference);
47134730
}
47144731

4715-
const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true);
4732+
// Get import attributes from the import/export statement
4733+
const importAttributes = getImportAttributesFromLocation(location);
4734+
4735+
const ambientModule = tryFindAmbientModule(moduleReference, /*withAugmentations*/ true, importAttributes);
47164736
if (ambientModule) {
47174737
return ambientModule;
47184738
}
@@ -4845,6 +4865,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
48454865
if (patternAmbientModules) {
48464866
const pattern = findBestPatternMatch(patternAmbientModules, _ => _.pattern, moduleReference);
48474867
if (pattern) {
4868+
// Check if the pattern module has matching import attributes
4869+
const patternSymbol = pattern.symbol;
4870+
if (patternSymbol.declarations) {
4871+
const hasMatchingDeclaration = patternSymbol.declarations.some(decl => {
4872+
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
4873+
return importAttributesMatch(importAttributes, decl.withClause);
4874+
}
4875+
return false;
4876+
});
4877+
if (!hasMatchingDeclaration) {
4878+
// Pattern matched but attributes don't match - continue searching
4879+
return undefined;
4880+
}
4881+
}
48484882
// If the module reference matched a pattern ambient module ('*.foo') but there's also a
48494883
// module augmentation by the specific name requested ('a.foo'), we store the merged symbol
48504884
// 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 {
1601016044
return result;
1601116045
}
1601216046

16013-
function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) {
16047+
function importAttributesMatch(importAttributes: ImportAttributes | undefined, moduleAttributes: ImportAttributes | undefined): boolean {
16048+
// If neither has attributes, they match
16049+
if (!importAttributes && !moduleAttributes) {
16050+
return true;
16051+
}
16052+
// If only one has attributes, they don't match
16053+
if (!importAttributes || !moduleAttributes) {
16054+
return false;
16055+
}
16056+
// Both have attributes - check if they match
16057+
// For now, we require exact match of all attributes
16058+
if (importAttributes.elements.length !== moduleAttributes.elements.length) {
16059+
return false;
16060+
}
16061+
// Create a map of module attributes for easier lookup
16062+
const moduleAttrsMap = new Map<string, string>();
16063+
for (const attr of moduleAttributes.elements) {
16064+
const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text;
16065+
const value = isStringLiteral(attr.value) ? attr.value.text : undefined;
16066+
if (value !== undefined) {
16067+
moduleAttrsMap.set(name, value);
16068+
}
16069+
}
16070+
// Check that all import attributes match
16071+
for (const attr of importAttributes.elements) {
16072+
const name = isIdentifier(attr.name) ? idText(attr.name) : attr.name.text;
16073+
const value = isStringLiteral(attr.value) ? attr.value.text : undefined;
16074+
if (value === undefined || moduleAttrsMap.get(name) !== value) {
16075+
return false;
16076+
}
16077+
}
16078+
return true;
16079+
}
16080+
16081+
function tryFindAmbientModule(moduleName: string, withAugmentations: boolean, importAttributes?: ImportAttributes) {
1601416082
if (isExternalModuleNameRelative(moduleName)) {
1601516083
return undefined;
1601616084
}
1601716085
const symbol = getSymbol(globals, '"' + moduleName + '"' as __String, SymbolFlags.ValueModule);
16086+
if (!symbol) {
16087+
return undefined;
16088+
}
16089+
// Check if the module declaration has matching import attributes
16090+
const declarations = symbol.declarations;
16091+
if (declarations) {
16092+
const hasMatchingDeclaration = declarations.some(decl => {
16093+
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
16094+
return importAttributesMatch(importAttributes, decl.withClause);
16095+
}
16096+
return false;
16097+
});
16098+
if (!hasMatchingDeclaration) {
16099+
return undefined;
16100+
}
16101+
}
1601816102
// merged symbol is module declaration symbol combined with all augmentations
1601916103
return symbol && withAugmentations ? getMergedSymbol(symbol) : symbol;
1602016104
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
test.ts(2,33): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
2+
test.ts(6,30): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
3+
test.ts(14,38): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
4+
test.ts(18,31): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
5+
test.ts(26,35): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
6+
test.ts(30,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
7+
test.ts(34,40): error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
8+
9+
10+
==== types.d.ts (0 errors) ====
11+
// Test exact match with attributes
12+
declare module "*.css" with { type: "css" } {
13+
const stylesheet: CSSStyleSheet;
14+
export default stylesheet;
15+
}
16+
17+
// Test exact match with different attributes
18+
declare module "*.json" with { type: "json" } {
19+
const data: any;
20+
export default data;
21+
}
22+
23+
// Test exact match without attributes
24+
declare module "*.txt" {
25+
const content: string;
26+
export default content;
27+
}
28+
29+
// Test multiple attributes
30+
declare module "*.wasm" with { type: "module", version: "1" } {
31+
const module: WebAssembly.Module;
32+
export default module;
33+
}
34+
35+
==== test.ts (7 errors) ====
36+
// Should resolve correctly - matching attributes
37+
import styles from "styles.css" with { type: "css" };
38+
~~~~~~~~~~~~~~~~~~~~
39+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
40+
styles; // Should be CSSStyleSheet
41+
42+
// Should resolve correctly - matching attributes
43+
import data from "data.json" with { type: "json" };
44+
~~~~~~~~~~~~~~~~~~~~~
45+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
46+
data; // Should be any
47+
48+
// Should resolve correctly - no attributes on either side
49+
import text from "file.txt";
50+
text; // Should be string
51+
52+
// Should resolve correctly - multiple matching attributes
53+
import wasmModule from "module.wasm" with { type: "module", version: "1" };
54+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
56+
wasmModule; // Should be WebAssembly.Module
57+
58+
// Should NOT resolve - import has attributes but declaration doesn't
59+
import text2 from "file2.txt" with { type: "text" };
60+
~~~~~~~~~~~~~~~~~~~~~
61+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
62+
text2; // Should be any (no match)
63+
64+
// Should NOT resolve - import has no attributes but declaration does
65+
import styles2 from "styles2.css";
66+
styles2; // Should be any (no match)
67+
68+
// Should NOT resolve - mismatched attribute values
69+
import styles3 from "styles3.css" with { type: "style" };
70+
~~~~~~~~~~~~~~~~~~~~~~
71+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
72+
styles3; // Should be any (no match)
73+
74+
// Should NOT resolve - missing attribute
75+
import wasmModule2 from "module2.wasm" with { type: "module" };
76+
~~~~~~~~~~~~~~~~~~~~~~~
77+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
78+
wasmModule2; // Should be any (no match - missing version attribute)
79+
80+
// Should NOT resolve - extra attribute
81+
import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" };
82+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
83+
!!! error TS2856: Import attributes are not allowed on statements that compile to CommonJS 'require' calls.
84+
wasmModule3; // Should be any (no match - extra attribute)
85+
86+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//// [tests/cases/conformance/ambient/ambientModuleWithImportAttributesSemantics.ts] ////
2+
3+
//// [types.d.ts]
4+
// Test exact match with attributes
5+
declare module "*.css" with { type: "css" } {
6+
const stylesheet: CSSStyleSheet;
7+
export default stylesheet;
8+
}
9+
10+
// Test exact match with different attributes
11+
declare module "*.json" with { type: "json" } {
12+
const data: any;
13+
export default data;
14+
}
15+
16+
// Test exact match without attributes
17+
declare module "*.txt" {
18+
const content: string;
19+
export default content;
20+
}
21+
22+
// Test multiple attributes
23+
declare module "*.wasm" with { type: "module", version: "1" } {
24+
const module: WebAssembly.Module;
25+
export default module;
26+
}
27+
28+
//// [test.ts]
29+
// Should resolve correctly - matching attributes
30+
import styles from "styles.css" with { type: "css" };
31+
styles; // Should be CSSStyleSheet
32+
33+
// Should resolve correctly - matching attributes
34+
import data from "data.json" with { type: "json" };
35+
data; // Should be any
36+
37+
// Should resolve correctly - no attributes on either side
38+
import text from "file.txt";
39+
text; // Should be string
40+
41+
// Should resolve correctly - multiple matching attributes
42+
import wasmModule from "module.wasm" with { type: "module", version: "1" };
43+
wasmModule; // Should be WebAssembly.Module
44+
45+
// Should NOT resolve - import has attributes but declaration doesn't
46+
import text2 from "file2.txt" with { type: "text" };
47+
text2; // Should be any (no match)
48+
49+
// Should NOT resolve - import has no attributes but declaration does
50+
import styles2 from "styles2.css";
51+
styles2; // Should be any (no match)
52+
53+
// Should NOT resolve - mismatched attribute values
54+
import styles3 from "styles3.css" with { type: "style" };
55+
styles3; // Should be any (no match)
56+
57+
// Should NOT resolve - missing attribute
58+
import wasmModule2 from "module2.wasm" with { type: "module" };
59+
wasmModule2; // Should be any (no match - missing version attribute)
60+
61+
// Should NOT resolve - extra attribute
62+
import wasmModule3 from "module3.wasm" with { type: "module", version: "1", extra: "value" };
63+
wasmModule3; // Should be any (no match - extra attribute)
64+
65+
66+
67+
//// [test.js]
68+
"use strict";
69+
var __importDefault = (this && this.__importDefault) || function (mod) {
70+
return (mod && mod.__esModule) ? mod : { "default": mod };
71+
};
72+
Object.defineProperty(exports, "__esModule", { value: true });
73+
// Should resolve correctly - matching attributes
74+
const styles_css_1 = __importDefault(require("styles.css"));
75+
styles_css_1.default; // Should be CSSStyleSheet
76+
// Should resolve correctly - matching attributes
77+
const data_json_1 = __importDefault(require("data.json"));
78+
data_json_1.default; // Should be any
79+
// Should resolve correctly - no attributes on either side
80+
const file_txt_1 = __importDefault(require("file.txt"));
81+
file_txt_1.default; // Should be string
82+
// Should resolve correctly - multiple matching attributes
83+
const module_wasm_1 = __importDefault(require("module.wasm"));
84+
module_wasm_1.default; // Should be WebAssembly.Module
85+
// Should NOT resolve - import has attributes but declaration doesn't
86+
const file2_txt_1 = __importDefault(require("file2.txt"));
87+
file2_txt_1.default; // Should be any (no match)
88+
// Should NOT resolve - import has no attributes but declaration does
89+
const styles2_css_1 = __importDefault(require("styles2.css"));
90+
styles2_css_1.default; // Should be any (no match)
91+
// Should NOT resolve - mismatched attribute values
92+
const styles3_css_1 = __importDefault(require("styles3.css"));
93+
styles3_css_1.default; // Should be any (no match)
94+
// Should NOT resolve - missing attribute
95+
const module2_wasm_1 = __importDefault(require("module2.wasm"));
96+
module2_wasm_1.default; // Should be any (no match - missing version attribute)
97+
// Should NOT resolve - extra attribute
98+
const module3_wasm_1 = __importDefault(require("module3.wasm"));
99+
module3_wasm_1.default; // Should be any (no match - extra attribute)

0 commit comments

Comments
 (0)