Skip to content

Commit 72d4973

Browse files
authored
fix(55994): Type-check Import Attributes in static imports (#56034)
1 parent 9999f26 commit 72d4973

16 files changed

+678
-4
lines changed

src/compiler/checker.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ import {
325325
getModeForUsageLocation,
326326
getModifiers,
327327
getModuleInstanceState,
328+
getNameFromImportAttribute,
328329
getNameFromIndexInfo,
329330
getNameOfDeclaration,
330331
getNameOfExpando,
@@ -407,6 +408,7 @@ import {
407408
IdentifierTypePredicate,
408409
idText,
409410
IfStatement,
411+
ImportAttribute,
410412
ImportAttributes,
411413
ImportCall,
412414
ImportClause,
@@ -553,6 +555,7 @@ import {
553555
isIdentifierTypePredicate,
554556
isIdentifierTypeReference,
555557
isIfStatement,
558+
isImportAttributes,
556559
isImportCall,
557560
isImportClause,
558561
isImportDeclaration,
@@ -2179,6 +2182,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
21792182
var deferredGlobalImportMetaType: ObjectType;
21802183
var deferredGlobalImportMetaExpressionType: ObjectType;
21812184
var deferredGlobalImportCallOptionsType: ObjectType | undefined;
2185+
var deferredGlobalImportAttributesType: ObjectType | undefined;
21822186
var deferredGlobalDisposableType: ObjectType | undefined;
21832187
var deferredGlobalAsyncDisposableType: ObjectType | undefined;
21842188
var deferredGlobalExtractSymbol: Symbol | undefined;
@@ -11555,6 +11559,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1155511559
return widenTypeForVariableLikeDeclaration(getTypeForVariableLikeDeclaration(declaration, /*includeOptionality*/ true, CheckMode.Normal), declaration, reportErrors);
1155611560
}
1155711561

11562+
function getTypeFromImportAttributes(node: ImportAttributes): Type {
11563+
const links = getNodeLinks(node);
11564+
if (!links.resolvedType) {
11565+
const symbol = createSymbol(SymbolFlags.ObjectLiteral, InternalSymbolName.ImportAttributes);
11566+
const members = createSymbolTable();
11567+
forEach(node.elements, attr => {
11568+
const member = createSymbol(SymbolFlags.Property, getNameFromImportAttribute(attr));
11569+
member.parent = symbol;
11570+
member.links.type = checkImportAttribute(attr);
11571+
member.links.target = member;
11572+
members.set(member.escapedName, member);
11573+
});
11574+
const type = createAnonymousType(symbol, members, emptyArray, emptyArray, emptyArray);
11575+
type.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.NonInferrableType;
11576+
links.resolvedType = type;
11577+
}
11578+
return links.resolvedType;
11579+
}
11580+
1155811581
function isGlobalSymbolConstructor(node: Node) {
1155911582
const symbol = getSymbolOfNode(node);
1156011583
const globalSymbol = getGlobalESSymbolConstructorTypeSymbol(/*reportErrors*/ false);
@@ -16417,6 +16440,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1641716440
return (deferredGlobalImportCallOptionsType ||= getGlobalType("ImportCallOptions" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType;
1641816441
}
1641916442

16443+
function getGlobalImportAttributesType(reportErrors: boolean) {
16444+
return (deferredGlobalImportAttributesType ||= getGlobalType("ImportAttributes" as __String, /*arity*/ 0, reportErrors)) || emptyObjectType;
16445+
}
16446+
1642016447
function getGlobalESSymbolConstructorSymbol(reportErrors: boolean): Symbol | undefined {
1642116448
return deferredGlobalESSymbolConstructorSymbol ||= getGlobalValueSymbol("Symbol" as __String, reportErrors);
1642216449
}
@@ -30904,6 +30931,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3090430931
case SyntaxKind.JsxOpeningElement:
3090530932
case SyntaxKind.JsxSelfClosingElement:
3090630933
return getContextualJsxElementAttributesType(parent as JsxOpeningLikeElement, contextFlags);
30934+
case SyntaxKind.ImportAttribute:
30935+
return getContextualImportAttributeType(parent as ImportAttribute);
3090730936
}
3090830937
return undefined;
3090930938
}
@@ -30950,6 +30979,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3095030979
}
3095130980
}
3095230981

30982+
function getContextualImportAttributeType(node: ImportAttribute) {
30983+
return getTypeOfPropertyOfContextualType(getGlobalImportAttributesType(/*reportErrors*/ false), getNameFromImportAttribute(node));
30984+
}
30985+
3095330986
function getContextualJsxElementAttributesType(node: JsxOpeningLikeElement, contextFlags: ContextFlags | undefined) {
3095430987
if (isJsxOpeningElement(node) && contextFlags !== ContextFlags.Completions) {
3095530988
const index = findContextualNode(node.parent, /*includeCaches*/ !contextFlags);
@@ -45991,6 +46024,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4599146024
function checkImportAttributes(declaration: ImportDeclaration | ExportDeclaration) {
4599246025
const node = declaration.attributes;
4599346026
if (node) {
46027+
const importAttributesType = getGlobalImportAttributesType(/*reportErrors*/ true);
46028+
if (importAttributesType !== emptyObjectType) {
46029+
checkTypeAssignableTo(getTypeFromImportAttributes(node), getNullableType(importAttributesType, TypeFlags.Undefined), node);
46030+
}
46031+
4599446032
const validForTypeAttributes = isExclusivelyTypeOnlyImportOrExport(declaration);
4599546033
const override = getResolutionModeOverride(node, validForTypeAttributes ? grammarErrorOnNode : undefined);
4599646034
const isImportAttributes = declaration.attributes.token === SyntaxKind.WithKeyword;
@@ -46020,6 +46058,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4602046058
}
4602146059
}
4602246060

46061+
function checkImportAttribute(node: ImportAttribute) {
46062+
return getRegularTypeOfLiteralType(checkExpressionCached(node.value));
46063+
}
46064+
4602346065
function checkImportDeclaration(node: ImportDeclaration) {
4602446066
if (checkGrammarModuleElementContext(node, isInJSFile(node) ? Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_module : Diagnostics.An_import_declaration_can_only_be_used_at_the_top_level_of_a_namespace_or_module)) {
4602546067
// If we hit an import declaration in an illegal context, just bail out to avoid cascading errors.
@@ -47700,6 +47742,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4770047742
return checkMetaPropertyKeyword(node.parent);
4770147743
}
4770247744

47745+
if (isImportAttributes(node)) {
47746+
return getGlobalImportAttributesType(/*reportErrors*/ false);
47747+
}
47748+
4770347749
return errorType;
4770447750
}
4770547751

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5971,6 +5971,7 @@ export const enum InternalSymbolName {
59715971
Default = "default", // Default export symbol (technically not wholly internal, but included here for usability)
59725972
This = "this",
59735973
InstantiationExpression = "__instantiationExpression", // Instantiation expressions
5974+
ImportAttributes = "__importAttributes",
59745975
}
59755976

59765977
/**

src/compiler/utilities.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ import {
220220
idText,
221221
IfStatement,
222222
ignoredPaths,
223+
ImportAttribute,
223224
ImportCall,
224225
ImportClause,
225226
ImportDeclaration,
@@ -10656,3 +10657,8 @@ export function replaceFirstStar(s: string, replacement: string): string {
1065610657
// Attempt to defeat this analysis by indirectly calling the method.
1065710658
return stringReplace.call(s, "*", replacement);
1065810659
}
10660+
10661+
/** @internal */
10662+
export function getNameFromImportAttribute(node: ImportAttribute) {
10663+
return isIdentifier(node.name) ? node.name.escapedText : escapeLeadingUnderscores(node.name.text);
10664+
}

src/harness/fourslashImpl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2448,7 +2448,13 @@ export class TestState {
24482448
const annotations = this.annotateContentWithTooltips(
24492449
result,
24502450
"completions",
2451-
item => item.optionalReplacementSpan,
2451+
item => {
2452+
if (item.optionalReplacementSpan) {
2453+
const { start, length } = item.optionalReplacementSpan;
2454+
return start && length === 0 ? { start, length: 1 } : item.optionalReplacementSpan;
2455+
}
2456+
return undefined;
2457+
},
24522458
item =>
24532459
item.entries?.flatMap(
24542460
entry =>

src/services/completions.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ import {
9595
getLineStartPositionForPosition,
9696
getLocalSymbolForExportDefault,
9797
getModifiers,
98+
getNameFromImportAttribute,
9899
getNameOfDeclaration,
99100
getNameTable,
100101
getNewLineCharacter,
@@ -169,6 +170,7 @@ import {
169170
isIdentifier,
170171
isIdentifierText,
171172
isImportableFile,
173+
isImportAttributes,
172174
isImportDeclaration,
173175
isImportEqualsDeclaration,
174176
isImportKeyword,
@@ -3768,6 +3770,7 @@ function getCompletionData(
37683770
|| tryGetObjectLikeCompletionSymbols()
37693771
|| tryGetImportCompletionSymbols()
37703772
|| tryGetImportOrExportClauseCompletionSymbols()
3773+
|| tryGetImportAttributesCompletionSymbols()
37713774
|| tryGetLocalNamedExportCompletionSymbols()
37723775
|| tryGetConstructorCompletion()
37733776
|| tryGetClassLikeCompletionSymbols()
@@ -4455,6 +4458,21 @@ function getCompletionData(
44554458
return GlobalsSearch.Success;
44564459
}
44574460

4461+
/**
4462+
* import { x } from "foo" with { | }
4463+
*/
4464+
function tryGetImportAttributesCompletionSymbols(): GlobalsSearch {
4465+
if (contextToken === undefined) return GlobalsSearch.Continue;
4466+
4467+
const importAttributes = contextToken.kind === SyntaxKind.OpenBraceToken || contextToken.kind === SyntaxKind.CommaToken ? tryCast(contextToken.parent, isImportAttributes) :
4468+
contextToken.kind === SyntaxKind.ColonToken ? tryCast(contextToken.parent.parent, isImportAttributes) : undefined;
4469+
if (importAttributes === undefined) return GlobalsSearch.Continue;
4470+
4471+
const existing = new Set(importAttributes.elements.map(getNameFromImportAttribute));
4472+
symbols = filter(typeChecker.getTypeAtLocation(importAttributes).getApparentProperties(), attr => !existing.has(attr.escapedName));
4473+
return GlobalsSearch.Success;
4474+
}
4475+
44584476
/**
44594477
* Adds local declarations for completions in named exports:
44604478
*

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7076,6 +7076,7 @@ declare namespace ts {
70767076
Default = "default",
70777077
This = "this",
70787078
InstantiationExpression = "__instantiationExpression",
7079+
ImportAttributes = "__importAttributes",
70797080
}
70807081
/**
70817082
* This represents a string whose leading underscore have been escaped by adding extra leading underscores.

0 commit comments

Comments
 (0)