diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index c584de5b475af..af6007f2369af 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1187,7 +1187,7 @@ function getNewImportFixes( const exportEquals = checker.resolveExternalModuleSymbol(exportInfo.moduleSymbol); let namespacePrefix; if (exportEquals !== exportInfo.moduleSymbol) { - namespacePrefix = forEachNameOfDefaultExport(exportEquals, checker, compilerOptions, /*preferCapitalizedNames*/ false, identity)!; + namespacePrefix = forEachNameOfDefaultExport(exportEquals, checker, getEmitScriptTarget(compilerOptions), identity)!; } namespacePrefix ||= moduleSymbolToValidIdentifier( exportInfo.moduleSymbol, @@ -1544,7 +1544,7 @@ function getExportInfos( if ( defaultInfo && symbolFlagsHaveMeaning(checker.getSymbolFlags(defaultInfo.symbol), currentTokenMeaning) - && forEachNameOfDefaultExport(defaultInfo.symbol, checker, compilerOptions, isJsxTagName, name => name === symbolName) + && forEachNameOfDefaultExport(defaultInfo.symbol, checker, getEmitScriptTarget(compilerOptions), (name, capitalizedName) => (isJsxTagName ? capitalizedName ?? name : name) === symbolName) ) { addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson); } diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 24d0c7b9b8000..4481e62a2bfea 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -4,7 +4,6 @@ import { append, arrayIsEqualTo, CancellationToken, - CompilerOptions, consumesNodeCoreModules, createMultiMap, Debug, @@ -18,9 +17,7 @@ import { GetCanonicalFileName, getDefaultLikeExportNameFromDeclaration, getDirectoryPath, - getEmitScriptTarget, getLocalSymbolForExportDefault, - getNamesForExportedSymbol, getNodeModulePathParts, getPackageNameFromTypesPackageName, getRegexFromPattern, @@ -46,6 +43,7 @@ import { Path, pathContainsNodeModules, Program, + ScriptTarget, skipAlias, SourceFile, startsWith, @@ -198,7 +196,7 @@ export function createCacheableExportInfoMap(host: CacheableExportInfoMapHost): // get a better name. const names = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol) ? unescapeLeadingUnderscores(symbolTableKey) - : getNamesForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined); + : getNamesForExportedSymbol(namedSymbol, checker, /*scriptTarget*/ undefined); const symbolName = typeof names === "string" ? names : names[0]; const capitalizedSymbolName = typeof names === "string" ? undefined : names[1]; @@ -558,12 +556,21 @@ function isImportableSymbol(symbol: Symbol, checker: TypeChecker) { return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !isKnownSymbol(symbol) && !isPrivateIdentifierSymbol(symbol); } +function getNamesForExportedSymbol(defaultExport: Symbol, checker: TypeChecker, scriptTarget: ScriptTarget | undefined) { + let names: string | string[] | undefined; + forEachNameOfDefaultExport(defaultExport, checker, scriptTarget, (name, capitalizedName) => { + names = capitalizedName ? [name, capitalizedName] : name; + return true; + }); + return Debug.checkDefined(names); +} + /** * @internal * May call `cb` multiple times with the same name. * Terminates when `cb` returns a truthy value. */ -export function forEachNameOfDefaultExport(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, preferCapitalizedNames: boolean, cb: (name: string) => T | undefined): T | undefined { +export function forEachNameOfDefaultExport(defaultExport: Symbol, checker: TypeChecker, scriptTarget: ScriptTarget | undefined, cb: (name: string, capitalizedName?: string) => T | undefined): T | undefined { let chain: Symbol[] | undefined; let current: Symbol | undefined = defaultExport; @@ -588,7 +595,10 @@ export function forEachNameOfDefaultExport(defaultExport: Symbol, checker: Ty for (const symbol of chain ?? emptyArray) { if (symbol.parent && isExternalModuleSymbol(symbol.parent)) { - const final = cb(moduleSymbolToValidIdentifier(symbol.parent, getEmitScriptTarget(compilerOptions), preferCapitalizedNames)); + const final = cb( + moduleSymbolToValidIdentifier(symbol.parent, scriptTarget, /*forceCapitalize*/ false), + moduleSymbolToValidIdentifier(symbol.parent, scriptTarget, /*forceCapitalize*/ true), + ); if (final) return final; } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 2d57952373316..584e8d0c15f49 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4024,22 +4024,13 @@ export function firstOrOnly(valueOrArray: T | readonly T[]): T { return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray; } -/** @internal */ -export function getNamesForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined): string | [lowercase: string, capitalized: string] { - if (needsNameFromDeclaration(symbol)) { - const fromDeclaration = getDefaultLikeExportNameFromDeclaration(symbol); - if (fromDeclaration) return fromDeclaration; - const fileNameCase = moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ false); - const capitalized = moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*forceCapitalize*/ true); - if (fileNameCase === capitalized) return fileNameCase; - return [fileNameCase, capitalized]; - } - return symbol.name; -} - -/** @internal */ +/** + * If a type checker and multiple files are available, consider using `forEachNameOfDefaultExport` + * instead, which searches for names of re-exported defaults/namespaces in target files. + * @internal + */ export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined, preferCapitalized?: boolean) { - if (needsNameFromDeclaration(symbol)) { + if (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default) { // Names for default exports: // - export default foo => foo // - export { foo as default } => foo @@ -4050,11 +4041,11 @@ export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTar return symbol.name; } -function needsNameFromDeclaration(symbol: Symbol) { - return !(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default); -} - -/** @internal */ +/** + * If a type checker and multiple files are available, consider using `forEachNameOfDefaultExport` + * instead, which searches for names of re-exported defaults/namespaces in target files. + * @internal + */ export function getDefaultLikeExportNameFromDeclaration(symbol: Symbol): string | undefined { return firstDefined(symbol.declarations, d => { // "export default" in this case. See `ExportAssignment`for more details. diff --git a/tests/baselines/reference/tsserver/fourslashServer/completionsImport_jsModuleExportsAssignment.js b/tests/baselines/reference/tsserver/fourslashServer/completionsImport_jsModuleExportsAssignment.js index 83f4bcc5e4de5..f69dc477c9e4f 100644 --- a/tests/baselines/reference/tsserver/fourslashServer/completionsImport_jsModuleExportsAssignment.js +++ b/tests/baselines/reference/tsserver/fourslashServer/completionsImport_jsModuleExportsAssignment.js @@ -380,7 +380,7 @@ Info seq [hh:mm:ss:mss] getCompletionData: Is inside comment: * Info seq [hh:mm:ss:mss] getCompletionData: Get previous token: * Info seq [hh:mm:ss:mss] getExportInfoMap: cache miss or empty; calculating new results Info seq [hh:mm:ss:mss] getExportInfoMap: done in * ms -Info seq [hh:mm:ss:mss] collectAutoImports: resolved 0 module specifiers, plus 0 ambient and 3 from cache +Info seq [hh:mm:ss:mss] collectAutoImports: resolved 0 module specifiers, plus 0 ambient and 4 from cache Info seq [hh:mm:ss:mss] collectAutoImports: response is incomplete Info seq [hh:mm:ss:mss] collectAutoImports: * Info seq [hh:mm:ss:mss] getCompletionData: Semantic work: * @@ -1053,6 +1053,19 @@ Info seq [hh:mm:ss:mss] response: "fileName": "/third_party/marked/src/defaults.js" } }, + { + "name": "defaults", + "kind": "property", + "kindModifiers": "", + "sortText": "16", + "hasAction": true, + "source": "/third_party/marked/src/defaults", + "data": { + "exportName": "export=", + "exportMapKey": "8 * defaults ", + "fileName": "/third_party/marked/src/defaults.js" + } + }, { "name": "defaults", "kind": "alias", @@ -1257,7 +1270,7 @@ Info seq [hh:mm:ss:mss] getCompletionData: Get current token: * Info seq [hh:mm:ss:mss] getCompletionData: Is inside comment: * Info seq [hh:mm:ss:mss] getCompletionData: Get previous token: * Info seq [hh:mm:ss:mss] getExportInfoMap: cache hit -Info seq [hh:mm:ss:mss] collectAutoImports: resolved 0 module specifiers, plus 0 ambient and 3 from cache +Info seq [hh:mm:ss:mss] collectAutoImports: resolved 0 module specifiers, plus 0 ambient and 4 from cache Info seq [hh:mm:ss:mss] collectAutoImports: response is incomplete Info seq [hh:mm:ss:mss] collectAutoImports: * Info seq [hh:mm:ss:mss] getCompletionData: Semantic work: * @@ -1943,6 +1956,19 @@ Info seq [hh:mm:ss:mss] response: "fileName": "/third_party/marked/src/defaults.js" } }, + { + "name": "defaults", + "kind": "property", + "kindModifiers": "", + "sortText": "16", + "hasAction": true, + "source": "/third_party/marked/src/defaults", + "data": { + "exportName": "export=", + "exportMapKey": "8 * defaults ", + "fileName": "/third_party/marked/src/defaults.js" + } + }, { "name": "defaults", "kind": "alias", diff --git a/tests/cases/fourslash/completionsImport_reExportDefault2.ts b/tests/cases/fourslash/completionsImport_reExportDefault2.ts new file mode 100644 index 0000000000000..85975f7f0d6ca --- /dev/null +++ b/tests/cases/fourslash/completionsImport_reExportDefault2.ts @@ -0,0 +1,39 @@ +/// + +// @module: preserve +// @checkJs: true + +// @Filename: /node_modules/example/package.json +//// { "name": "example", "version": "1.0.0", "main": "dist/index.js" } + +// @Filename: /node_modules/example/dist/nested/module.d.ts +//// declare const defaultExport: () => void; +//// declare const namedExport: () => void; +//// +//// export default defaultExport; +//// export { namedExport }; + +// @Filename: /node_modules/example/dist/index.d.ts +//// export { default, namedExport } from "./nested/module"; + +// @Filename: /index.mjs +//// import { namedExport } from "example"; +//// defaultExp/**/ + +verify.completions({ + marker: "", + exact: completion.globalsInJsPlus([ + "namedExport", + { + name: "defaultExport", + source: "example", + sourceDisplay: "example", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ]), + preferences: { + includeCompletionsForModuleExports: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file