Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor default export info name gathering #58460

Merged
merged 3 commits into from
May 8, 2024
Merged
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
1 change: 1 addition & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading