Skip to content

Commit

Permalink
Refactor default export info name gathering (#58460)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch committed May 8, 2024
1 parent b9c71c3 commit bc14459
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 116 deletions.
1 change: 1 addition & 0 deletions src/compiler/checker.ts
Expand Up @@ -1900,6 +1900,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
getMemberOverrideModifierStatus,
isTypeParameterPossiblyReferenced,
typeHasCallOrConstructSignatures,
getSymbolFlags,
};

function getCandidateSignaturesForStringLiteralCompletions(call: CallLikeExpression, editingArgument: Node) {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Expand Up @@ -5361,6 +5361,7 @@ export interface TypeChecker {
/** @internal */ getMemberOverrideModifierStatus(node: ClassLikeDeclaration, member: ClassElement, memberSymbol: Symbol): MemberOverrideStatus;
/** @internal */ isTypeParameterPossiblyReferenced(tp: TypeParameter, node: Node): boolean;
/** @internal */ typeHasCallOrConstructSignatures(type: Type): boolean;
/** @internal */ getSymbolFlags(symbol: Symbol): SymbolFlags;
}

/** @internal */
Expand Down
2 changes: 1 addition & 1 deletion src/services/codefixes/convertToEsModule.ts
@@ -1,6 +1,5 @@
import {
createCodeFixActionWithoutFixAll,
moduleSpecifierToValidIdentifier,
registerCodeFix,
} from "../_namespaces/ts.codefix.js";
import {
Expand Down Expand Up @@ -59,6 +58,7 @@ import {
mapIterator,
MethodDeclaration,
Modifier,
moduleSpecifierToValidIdentifier,
Node,
NodeArray,
NodeFlags,
Expand Down
70 changes: 18 additions & 52 deletions src/services/codefixes/importFixes.ts
Expand Up @@ -41,12 +41,12 @@ import {
flatMap,
flatMapIterator,
forEachExternalModuleToImportFrom,
forEachNameOfDefaultExport,
formatting,
FutureSourceFile,
FutureSymbolExportInfo,
getAllowSyntheticDefaultImports,
getBaseFileName,
getDefaultExportInfoWorker,
getDefaultLikeExportInfo,
getDirectoryPath,
getEmitModuleFormatOfFileWorker,
Expand All @@ -55,7 +55,6 @@ import {
getEmitScriptTarget,
getExportInfoMap,
getImpliedNodeFormatForEmitWorker,
getMeaningFromDeclaration,
getMeaningFromLocation,
getNameForExportedSymbol,
getOutputExtension,
Expand All @@ -71,6 +70,7 @@ import {
hasJSFileExtension,
hostGetCanonicalFileName,
Identifier,
identity,
ImportClause,
ImportEqualsDeclaration,
importFromModuleSpecifier,
Expand All @@ -81,8 +81,6 @@ import {
isExternalModuleReference,
isFullSourceFile,
isIdentifier,
isIdentifierPart,
isIdentifierStart,
isImportableFile,
isImportDeclaration,
isImportEqualsDeclaration,
Expand All @@ -96,7 +94,6 @@ import {
isNamespaceImport,
isRequireVariableStatement,
isSourceFileJS,
isStringANonContextualKeyword,
isStringLiteral,
isStringLiteralLike,
isTypeOnlyImportDeclaration,
Expand All @@ -114,6 +111,7 @@ import {
ModuleKind,
moduleResolutionUsesNodeModules,
moduleSpecifiers,
moduleSymbolToValidIdentifier,
MultiMap,
Mutable,
NamedImports,
Expand All @@ -129,12 +127,9 @@ import {
pathIsBareSpecifier,
Program,
QuotePreference,
removeFileExtension,
removeSuffix,
RequireOrImportCall,
RequireVariableStatement,
sameMap,
ScriptTarget,
SemanticMeaning,
shouldUseUriStyleNodeCoreModules,
single,
Expand Down Expand Up @@ -873,7 +868,6 @@ function getAllExportInfoForSymbol(importingFile: SourceFile | FutureSourceFile,
}

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

function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined {
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker);
if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) {
return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson };
}
Expand Down Expand Up @@ -1194,7 +1188,7 @@ function getNewImportFixes(
const exportEquals = checker.resolveExternalModuleSymbol(exportInfo.moduleSymbol);
let namespacePrefix;
if (exportEquals !== exportInfo.moduleSymbol) {
namespacePrefix = getDefaultExportInfoWorker(exportEquals, checker, compilerOptions)?.name;
namespacePrefix = forEachNameOfDefaultExport(exportEquals, checker, compilerOptions, /*preferCapitalizedNames*/ false, identity)!;
}
namespacePrefix ||= moduleSymbolToValidIdentifier(
exportInfo.moduleSymbol,
Expand Down Expand Up @@ -1533,14 +1527,18 @@ function getExportInfos(
cancellationToken.throwIfCancellationRequested();

const compilerOptions = program.getCompilerOptions();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && symbolHasMeaning(defaultInfo.resolvedSymbol, currentTokenMeaning)) {
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker);
if (
defaultInfo
&& symbolFlagsHaveMeaning(checker.getSymbolFlags(defaultInfo.symbol), currentTokenMeaning)
&& forEachNameOfDefaultExport(defaultInfo.symbol, checker, compilerOptions, isJsxTagName, name => name === symbolName)
) {
addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson);
}

// check exports with the same name
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol);
if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
if (exportSymbolWithIdenticalName && symbolFlagsHaveMeaning(checker.getSymbolFlags(exportSymbolWithIdenticalName), currentTokenMeaning)) {
addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, isFromPackageJson);
}
});
Expand Down Expand Up @@ -2009,44 +2007,12 @@ function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern
) as RequireVariableStatement;
}

function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}

/** @internal */
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize);
}

/** @internal */
export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined, forceCapitalize?: boolean): string {
const baseName = getBaseFileName(removeSuffix(moduleSpecifier, "/index"));
let res = "";
let lastCharWasValid = true;
const firstCharCode = baseName.charCodeAt(0);
if (isIdentifierStart(firstCharCode, target)) {
res += String.fromCharCode(firstCharCode);
if (forceCapitalize) {
res = res.toUpperCase();
}
}
else {
lastCharWasValid = false;
}
for (let i = 1; i < baseName.length; i++) {
const ch = baseName.charCodeAt(i);
const isValid = isIdentifierPart(ch, target);
if (isValid) {
let char = String.fromCharCode(ch);
if (!lastCharWasValid) {
char = char.toUpperCase();
}
res += char;
}
lastCharWasValid = isValid;
}
// Need `|| "_"` to ensure result isn't empty.
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
function symbolFlagsHaveMeaning(flags: SymbolFlags, meaning: SemanticMeaning): boolean {
return meaning === SemanticMeaning.All ? true :
meaning & SemanticMeaning.Value ? !!(flags & SymbolFlags.Value) :
meaning & SemanticMeaning.Type ? !!(flags & SymbolFlags.Type) :
meaning & SemanticMeaning.Namespace ? !!(flags & SymbolFlags.Namespace) :
false;
}

function getImpliedNodeFormatForEmit(file: SourceFile | FutureSourceFile, program: Program) {
Expand Down
92 changes: 38 additions & 54 deletions src/services/exportInfoMap.ts
@@ -1,6 +1,7 @@
import {
__String,
addToSeen,
append,
arrayIsEqualTo,
CancellationToken,
CompilerOptions,
Expand All @@ -10,15 +11,15 @@ import {
emptyArray,
ensureTrailingDirectorySeparator,
findIndex,
firstDefined,
forEachAncestorDirectory,
forEachEntry,
FutureSourceFile,
getBaseFileName,
GetCanonicalFileName,
getDefaultLikeExportNameFromDeclaration,
getDirectoryPath,
getEmitScriptTarget,
getLocalSymbolForExportDefault,
getNameForExportedSymbol,
getNamesForExportedSymbol,
getNodeModulePathParts,
getPackageNameFromTypesPackageName,
Expand All @@ -28,12 +29,9 @@ import {
hostGetCanonicalFileName,
hostUsesCaseSensitiveFileNames,
InternalSymbolName,
isExportAssignment,
isExportSpecifier,
isExternalModuleNameRelative,
isExternalModuleSymbol,
isExternalOrCommonJsModule,
isIdentifier,
isKnownSymbol,
isNonGlobalAmbientModule,
isPrivateIdentifierSymbol,
Expand All @@ -42,21 +40,20 @@ import {
ModuleSpecifierCache,
ModuleSpecifierResolutionHost,
moduleSpecifiers,
moduleSymbolToValidIdentifier,
nodeModulesPathPart,
PackageJsonImportFilter,
Path,
pathContainsNodeModules,
Program,
skipAlias,
skipOuterExpressions,
SourceFile,
startsWith,
Statement,
stripQuotes,
Symbol,
SymbolFlags,
timestamp,
tryCast,
TypeChecker,
unescapeLeadingUnderscores,
unmangleScopedPackageName,
Expand Down Expand Up @@ -502,14 +499,13 @@ export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, h
}

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

/** @internal */
export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
if (!exported) return undefined;
const { symbol, exportKind } = exported;
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
return info && { symbol, exportKind, ...info };
export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker) {
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
}

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

function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol; readonly exportKind: ExportKind; } | undefined {
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
}
/**
* @internal
* May call `cb` multiple times with the same name.
* Terminates when `cb` returns a truthy value.
*/
export function forEachNameOfDefaultExport<T>(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, preferCapitalizedNames: boolean, cb: (name: string) => T | undefined): T | undefined {
let chain: Symbol[] | undefined;
let current: Symbol | undefined = defaultExport;

while (current) {
// The predecessor to this function also looked for a name on the `localSymbol`
// of default exports, but I think `getDefaultLikeExportNameFromDeclaration`
// accomplishes the same thing via syntax - no tests failed when I removed it.
const fromDeclaration = getDefaultLikeExportNameFromDeclaration(current);
if (fromDeclaration) {
const final = cb(fromDeclaration);
if (final) return final;
}

/** @internal */
export function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly resolvedSymbol: Symbol; readonly name: string; } | undefined {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol) return { resolvedSymbol: localSymbol, name: localSymbol.name };

const name = getNameForExportDefault(defaultExport);
if (name !== undefined) return { resolvedSymbol: defaultExport, name };

if (defaultExport.flags & SymbolFlags.Alias) {
const aliased = checker.getImmediateAliasedSymbol(defaultExport);
if (aliased && aliased.parent) {
// - `aliased` will be undefined if the module is exporting an unresolvable name,
// but we can still offer completions for it.
// - `aliased.parent` will be undefined if the module is exporting `globalThis.something`,
// or another expression that resolves to a global.
return getDefaultExportInfoWorker(aliased, checker, compilerOptions);
if (current.escapedName !== InternalSymbolName.Default && current.escapedName !== InternalSymbolName.ExportEquals) {
const final = cb(current.name);
if (final) return final;
}
}

if (
defaultExport.escapedName !== InternalSymbolName.Default &&
defaultExport.escapedName !== InternalSymbolName.ExportEquals
) {
return { resolvedSymbol: defaultExport, name: defaultExport.getName() };
chain = append(chain, current);
current = current.flags & SymbolFlags.Alias ? checker.getImmediateAliasedSymbol(current) : undefined;
}
return { resolvedSymbol: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) };
}

function getNameForExportDefault(symbol: Symbol): string | undefined {
return symbol.declarations && firstDefined(symbol.declarations, declaration => {
if (isExportAssignment(declaration)) {
return tryCast(skipOuterExpressions(declaration.expression), isIdentifier)?.text;
}
else if (isExportSpecifier(declaration)) {
Debug.assert(declaration.name.text === InternalSymbolName.Default, "Expected the specifier to be a default export");
return declaration.propertyName && declaration.propertyName.text;
for (const symbol of chain ?? emptyArray) {
if (symbol.parent && isExternalModuleSymbol(symbol.parent)) {
const final = cb(moduleSymbolToValidIdentifier(symbol.parent, getEmitScriptTarget(compilerOptions), preferCapitalizedNames));
if (final) return final;
}
});
}
}
4 changes: 2 additions & 2 deletions src/services/refactors/convertImport.ts
@@ -1,7 +1,6 @@
import {
ApplicableRefactorInfo,
arrayFrom,
codefix,
Debug,
Diagnostics,
emptyArray,
Expand Down Expand Up @@ -29,6 +28,7 @@ import {
isPropertyAccessOrQualifiedName,
isShorthandPropertyAssignment,
isStringLiteral,
moduleSpecifierToValidIdentifier,
NamedImports,
NamespaceImport,
or,
Expand Down Expand Up @@ -222,7 +222,7 @@ export function doChangeNamedToNamespaceOrDefault(sourceFile: SourceFile, progra
toConvertSymbols.add(symbol);
}
});
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean {
// We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict.
// 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.
Expand Down

0 comments on commit bc14459

Please sign in to comment.