Skip to content

Commit bc14459

Browse files
authored
Refactor default export info name gathering (microsoft#58460)
1 parent b9c71c3 commit bc14459

File tree

11 files changed

+204
-116
lines changed

11 files changed

+204
-116
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,6 +1900,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
19001900
getMemberOverrideModifierStatus,
19011901
isTypeParameterPossiblyReferenced,
19021902
typeHasCallOrConstructSignatures,
1903+
getSymbolFlags,
19031904
};
19041905

19051906
function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5361,6 +5361,7 @@ export interface TypeChecker {
53615361
/** @internal */ getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement, memberSymbol: Symbol): MemberOverrideStatus;
53625362
/** @internal */ isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node): boolean;
53635363
/** @internal */ typeHasCallOrConstructSignatures(type: Type): boolean;
5364+
/** @internal */ getSymbolFlags(symbol: Symbol): SymbolFlags;
53645365
}
53655366

53665367
/** @internal */

src/services/codefixes/convertToEsModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
createCodeFixActionWithoutFixAll,
3-
moduleSpecifierToValidIdentifier,
43
registerCodeFix,
54
} from "../_namespaces/ts.codefix.js";
65
import {
@@ -59,6 +58,7 @@ import {
5958
mapIterator,
6059
MethodDeclaration,
6160
Modifier,
61+
moduleSpecifierToValidIdentifier,
6262
Node,
6363
NodeArray,
6464
NodeFlags,

src/services/codefixes/importFixes.ts

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ import {
4141
flatMap,
4242
flatMapIterator,
4343
forEachExternalModuleToImportFrom,
44+
forEachNameOfDefaultExport,
4445
formatting,
4546
FutureSourceFile,
4647
FutureSymbolExportInfo,
4748
getAllowSyntheticDefaultImports,
4849
getBaseFileName,
49-
getDefaultExportInfoWorker,
5050
getDefaultLikeExportInfo,
5151
getDirectoryPath,
5252
getEmitModuleFormatOfFileWorker,
@@ -55,7 +55,6 @@ import {
5555
getEmitScriptTarget,
5656
getExportInfoMap,
5757
getImpliedNodeFormatForEmitWorker,
58-
getMeaningFromDeclaration,
5958
getMeaningFromLocation,
6059
getNameForExportedSymbol,
6160
getOutputExtension,
@@ -71,6 +70,7 @@ import {
7170
hasJSFileExtension,
7271
hostGetCanonicalFileName,
7372
Identifier,
73+
identity,
7474
ImportClause,
7575
ImportEqualsDeclaration,
7676
importFromModuleSpecifier,
@@ -81,8 +81,6 @@ import {
8181
isExternalModuleReference,
8282
isFullSourceFile,
8383
isIdentifier,
84-
isIdentifierPart,
85-
isIdentifierStart,
8684
isImportableFile,
8785
isImportDeclaration,
8886
isImportEqualsDeclaration,
@@ -96,7 +94,6 @@ import {
9694
isNamespaceImport,
9795
isRequireVariableStatement,
9896
isSourceFileJS,
99-
isStringANonContextualKeyword,
10097
isStringLiteral,
10198
isStringLiteralLike,
10299
isTypeOnlyImportDeclaration,
@@ -114,6 +111,7 @@ import {
114111
ModuleKind,
115112
moduleResolutionUsesNodeModules,
116113
moduleSpecifiers,
114+
moduleSymbolToValidIdentifier,
117115
MultiMap,
118116
Mutable,
119117
NamedImports,
@@ -129,12 +127,9 @@ import {
129127
pathIsBareSpecifier,
130128
Program,
131129
QuotePreference,
132-
removeFileExtension,
133-
removeSuffix,
134130
RequireOrImportCall,
135131
RequireVariableStatement,
136132
sameMap,
137-
ScriptTarget,
138133
SemanticMeaning,
139134
shouldUseUriStyleNodeCoreModules,
140135
single,
@@ -873,7 +868,6 @@ function getAllExportInfoForSymbol(importingFile: SourceFile | FutureSourceFile,
873868
}
874869

875870
function getSingleExportInfoForSymbol(symbol: Symbol, symbolName: string, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo {
876-
const compilerOptions = program.getCompilerOptions();
877871
const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false);
878872
if (mainProgramInfo) {
879873
return mainProgramInfo;
@@ -882,7 +876,7 @@ function getSingleExportInfoForSymbol(symbol: Symbol, symbolName: string, module
882876
return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`);
883877

884878
function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined {
885-
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
879+
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker);
886880
if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) {
887881
return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson };
888882
}
@@ -1194,7 +1188,7 @@ function getNewImportFixes(
11941188
const exportEquals = checker.resolveExternalModuleSymbol(exportInfo.moduleSymbol);
11951189
let namespacePrefix;
11961190
if (exportEquals !== exportInfo.moduleSymbol) {
1197-
namespacePrefix = getDefaultExportInfoWorker(exportEquals, checker, compilerOptions)?.name;
1191+
namespacePrefix = forEachNameOfDefaultExport(exportEquals, checker, compilerOptions, /*preferCapitalizedNames*/ false, identity)!;
11981192
}
11991193
namespacePrefix ||= moduleSymbolToValidIdentifier(
12001194
exportInfo.moduleSymbol,
@@ -1533,14 +1527,18 @@ function getExportInfos(
15331527
cancellationToken.throwIfCancellationRequested();
15341528

15351529
const compilerOptions = program.getCompilerOptions();
1536-
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
1537-
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && symbolHasMeaning(defaultInfo.resolvedSymbol, currentTokenMeaning)) {
1530+
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker);
1531+
if (
1532+
defaultInfo
1533+
&& symbolFlagsHaveMeaning(checker.getSymbolFlags(defaultInfo.symbol), currentTokenMeaning)
1534+
&& forEachNameOfDefaultExport(defaultInfo.symbol, checker, compilerOptions, isJsxTagName, name => name === symbolName)
1535+
) {
15381536
addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson);
15391537
}
15401538

15411539
// check exports with the same name
15421540
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol);
1543-
if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
1541+
if (exportSymbolWithIdenticalName && symbolFlagsHaveMeaning(checker.getSymbolFlags(exportSymbolWithIdenticalName), currentTokenMeaning)) {
15441542
addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, isFromPackageJson);
15451543
}
15461544
});
@@ -2009,44 +2007,12 @@ function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern
20092007
) as RequireVariableStatement;
20102008
}
20112009

2012-
function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean {
2013-
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
2014-
}
2015-
2016-
/** @internal */
2017-
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string {
2018-
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize);
2019-
}
2020-
2021-
/** @internal */
2022-
export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined, forceCapitalize?: boolean): string {
2023-
const baseName = getBaseFileName(removeSuffix(moduleSpecifier, "/index"));
2024-
let res = "";
2025-
let lastCharWasValid = true;
2026-
const firstCharCode = baseName.charCodeAt(0);
2027-
if (isIdentifierStart(firstCharCode, target)) {
2028-
res += String.fromCharCode(firstCharCode);
2029-
if (forceCapitalize) {
2030-
res = res.toUpperCase();
2031-
}
2032-
}
2033-
else {
2034-
lastCharWasValid = false;
2035-
}
2036-
for (let i = 1; i < baseName.length; i++) {
2037-
const ch = baseName.charCodeAt(i);
2038-
const isValid = isIdentifierPart(ch, target);
2039-
if (isValid) {
2040-
let char = String.fromCharCode(ch);
2041-
if (!lastCharWasValid) {
2042-
char = char.toUpperCase();
2043-
}
2044-
res += char;
2045-
}
2046-
lastCharWasValid = isValid;
2047-
}
2048-
// Need `|| "_"` to ensure result isn't empty.
2049-
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
2010+
function symbolFlagsHaveMeaning(flags: SymbolFlags, meaning: SemanticMeaning): boolean {
2011+
return meaning === SemanticMeaning.All ? true :
2012+
meaning & SemanticMeaning.Value ? !!(flags & SymbolFlags.Value) :
2013+
meaning & SemanticMeaning.Type ? !!(flags & SymbolFlags.Type) :
2014+
meaning & SemanticMeaning.Namespace ? !!(flags & SymbolFlags.Namespace) :
2015+
false;
20502016
}
20512017

20522018
function getImpliedNodeFormatForEmit(file: SourceFile | FutureSourceFile, program: Program) {

src/services/exportInfoMap.ts

Lines changed: 38 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
__String,
33
addToSeen,
4+
append,
45
arrayIsEqualTo,
56
CancellationToken,
67
CompilerOptions,
@@ -10,15 +11,15 @@ import {
1011
emptyArray,
1112
ensureTrailingDirectorySeparator,
1213
findIndex,
13-
firstDefined,
1414
forEachAncestorDirectory,
1515
forEachEntry,
1616
FutureSourceFile,
1717
getBaseFileName,
1818
GetCanonicalFileName,
19+
getDefaultLikeExportNameFromDeclaration,
1920
getDirectoryPath,
21+
getEmitScriptTarget,
2022
getLocalSymbolForExportDefault,
21-
getNameForExportedSymbol,
2223
getNamesForExportedSymbol,
2324
getNodeModulePathParts,
2425
getPackageNameFromTypesPackageName,
@@ -28,12 +29,9 @@ import {
2829
hostGetCanonicalFileName,
2930
hostUsesCaseSensitiveFileNames,
3031
InternalSymbolName,
31-
isExportAssignment,
32-
isExportSpecifier,
3332
isExternalModuleNameRelative,
3433
isExternalModuleSymbol,
3534
isExternalOrCommonJsModule,
36-
isIdentifier,
3735
isKnownSymbol,
3836
isNonGlobalAmbientModule,
3937
isPrivateIdentifierSymbol,
@@ -42,21 +40,20 @@ import {
4240
ModuleSpecifierCache,
4341
ModuleSpecifierResolutionHost,
4442
moduleSpecifiers,
43+
moduleSymbolToValidIdentifier,
4544
nodeModulesPathPart,
4645
PackageJsonImportFilter,
4746
Path,
4847
pathContainsNodeModules,
4948
Program,
5049
skipAlias,
51-
skipOuterExpressions,
5250
SourceFile,
5351
startsWith,
5452
Statement,
5553
stripQuotes,
5654
Symbol,
5755
SymbolFlags,
5856
timestamp,
59-
tryCast,
6057
TypeChecker,
6158
unescapeLeadingUnderscores,
6259
unmangleScopedPackageName,
@@ -502,14 +499,13 @@ export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, h
502499
}
503500

504501
host.log?.("getExportInfoMap: cache miss or empty; calculating new results");
505-
const compilerOptions = program.getCompilerOptions();
506502
let moduleCount = 0;
507503
try {
508504
forEachExternalModuleToImportFrom(program, host, preferences, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
509505
if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested();
510506
const seenExports = new Map<__String, true>();
511507
const checker = program.getTypeChecker();
512-
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
508+
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker);
513509
// Note: I think we shouldn't actually see resolved module symbols here, but weird merges
514510
// can cause it to happen: see 'completionsImport_mergedReExport.ts'
515511
if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) {
@@ -551,61 +547,49 @@ export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, h
551547
}
552548

553549
/** @internal */
554-
export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
555-
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
556-
if (!exported) return undefined;
557-
const { symbol, exportKind } = exported;
558-
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
559-
return info && { symbol, exportKind, ...info };
550+
export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker) {
551+
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
552+
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
553+
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
554+
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
560555
}
561556

562557
function isImportableSymbol(symbol: Symbol, checker: TypeChecker) {
563558
return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !isKnownSymbol(symbol) && !isPrivateIdentifierSymbol(symbol);
564559
}
565560

566-
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol; readonly exportKind: ExportKind; } | undefined {
567-
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
568-
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
569-
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
570-
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
571-
}
561+
/**
562+
* @internal
563+
* May call `cb` multiple times with the same name.
564+
* Terminates when `cb` returns a truthy value.
565+
*/
566+
export function forEachNameOfDefaultExport<T>(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, preferCapitalizedNames: boolean, cb: (name: string) => T | undefined): T | undefined {
567+
let chain: Symbol[] | undefined;
568+
let current: Symbol | undefined = defaultExport;
569+
570+
while (current) {
571+
// The predecessor to this function also looked for a name on the `localSymbol`
572+
// of default exports, but I think `getDefaultLikeExportNameFromDeclaration`
573+
// accomplishes the same thing via syntax - no tests failed when I removed it.
574+
const fromDeclaration = getDefaultLikeExportNameFromDeclaration(current);
575+
if (fromDeclaration) {
576+
const final = cb(fromDeclaration);
577+
if (final) return final;
578+
}
572579

573-
/** @internal */
574-
export function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly resolvedSymbol: Symbol; readonly name: string; } | undefined {
575-
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
576-
if (localSymbol) return { resolvedSymbol: localSymbol, name: localSymbol.name };
577-
578-
const name = getNameForExportDefault(defaultExport);
579-
if (name !== undefined) return { resolvedSymbol: defaultExport, name };
580-
581-
if (defaultExport.flags & SymbolFlags.Alias) {
582-
const aliased = checker.getImmediateAliasedSymbol(defaultExport);
583-
if (aliased && aliased.parent) {
584-
// - `aliased` will be undefined if the module is exporting an unresolvable name,
585-
// but we can still offer completions for it.
586-
// - `aliased.parent` will be undefined if the module is exporting `globalThis.something`,
587-
// or another expression that resolves to a global.
588-
return getDefaultExportInfoWorker(aliased, checker, compilerOptions);
580+
if (current.escapedName !== InternalSymbolName.Default && current.escapedName !== InternalSymbolName.ExportEquals) {
581+
const final = cb(current.name);
582+
if (final) return final;
589583
}
590-
}
591584

592-
if (
593-
defaultExport.escapedName !== InternalSymbolName.Default &&
594-
defaultExport.escapedName !== InternalSymbolName.ExportEquals
595-
) {
596-
return { resolvedSymbol: defaultExport, name: defaultExport.getName() };
585+
chain = append(chain, current);
586+
current = current.flags & SymbolFlags.Alias ? checker.getImmediateAliasedSymbol(current) : undefined;
597587
}
598-
return { resolvedSymbol: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) };
599-
}
600588

601-
function getNameForExportDefault(symbol: Symbol): string | undefined {
602-
return symbol.declarations && firstDefined(symbol.declarations, declaration => {
603-
if (isExportAssignment(declaration)) {
604-
return tryCast(skipOuterExpressions(declaration.expression), isIdentifier)?.text;
605-
}
606-
else if (isExportSpecifier(declaration)) {
607-
Debug.assert(declaration.name.text === InternalSymbolName.Default, "Expected the specifier to be a default export");
608-
return declaration.propertyName && declaration.propertyName.text;
589+
for (const symbol of chain ?? emptyArray) {
590+
if (symbol.parent && isExternalModuleSymbol(symbol.parent)) {
591+
const final = cb(moduleSymbolToValidIdentifier(symbol.parent, getEmitScriptTarget(compilerOptions), preferCapitalizedNames));
592+
if (final) return final;
609593
}
610-
});
594+
}
611595
}

src/services/refactors/convertImport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
ApplicableRefactorInfo,
33
arrayFrom,
4-
codefix,
54
Debug,
65
Diagnostics,
76
emptyArray,
@@ -29,6 +28,7 @@ import {
2928
isPropertyAccessOrQualifiedName,
3029
isShorthandPropertyAssignment,
3130
isStringLiteral,
31+
moduleSpecifierToValidIdentifier,
3232
NamedImports,
3333
NamespaceImport,
3434
or,
@@ -222,7 +222,7 @@ export function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, progra
222222
toConvertSymbols.add(symbol);
223223
}
224224
});
225-
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
225+
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
226226
function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean {
227227
// We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict.
228228
// A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope.

0 commit comments

Comments
 (0)