diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 75df0d653be0d..217eb47cfa627 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3656,8 +3656,8 @@ namespace ts { if (exportEquals !== moduleSymbol) { const type = getTypeOfSymbol(exportEquals); if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { - getPropertiesOfType(type).forEach(symbol => { - cb(symbol, symbol.escapedName); + forEachPropertyOfType(type, (symbol, escapedName) => { + cb(symbol, escapedName); }); } } @@ -4033,13 +4033,17 @@ namespace ts { function getNamedMembers(members: SymbolTable): Symbol[] { let result: Symbol[] | undefined; members.forEach((symbol, id) => { - if (!isReservedMemberName(id) && symbolIsValue(symbol)) { + if (isNamedMember(symbol, id)) { (result || (result = [])).push(symbol); } }); return result || emptyArray; } + function isNamedMember(member: Symbol, escapedName: __String) { + return !isReservedMemberName(escapedName) && symbolIsValue(member); + } + function getNamedOrIndexSignatureMembers(members: SymbolTable): Symbol[] { const result = getNamedMembers(members); const index = getIndexSymbolFromSymbolTable(members); @@ -11571,6 +11575,17 @@ namespace ts { getPropertiesOfObjectType(type); } + function forEachPropertyOfType(type: Type, action: (symbol: Symbol, escapedName: __String) => void): void { + type = getReducedApparentType(type); + if (type.flags & TypeFlags.StructuredType) { + resolveStructuredTypeMembers(type as StructuredType).members.forEach((symbol, escapedName) => { + if (isNamedMember(symbol, escapedName)) { + action(symbol, escapedName); + } + }); + } + } + function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean { const list = obj.properties as NodeArray; return list.some(property => { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 89b673097b70c..0b95a88b9307e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -268,7 +268,7 @@ namespace ts.codefix { } export function getImportCompletionAction( - exportedSymbol: Symbol, + targetSymbol: Symbol, moduleSymbol: Symbol, sourceFile: SourceFile, symbolName: string, @@ -280,8 +280,8 @@ namespace ts.codefix { ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { const compilerOptions = program.getCompilerOptions(); const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name)) - ? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)] - : getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true); + ? [getSymbolExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)] + : getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true); const useRequire = shouldUseRequire(sourceFile, program); const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, isValidTypeOnlyUseSite, useRequire, host, preferences)); @@ -326,7 +326,7 @@ namespace ts.codefix { } } - function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] { + function getAllReExportingModules(importingFile: SourceFile, targetSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] { const result: SymbolExportInfo[] = []; const compilerOptions = program.getCompilerOptions(); const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { @@ -341,12 +341,12 @@ namespace ts.codefix { } const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); - if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) { + if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) { result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(defaultInfo.symbol, checker).flags, isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { - if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) { + if (exported.name === symbolName && checker.getMergedSymbol(skipAlias(exported, checker)) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) { result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, targetFlags: skipAlias(exported, checker).flags, isFromPackageJson }); } } diff --git a/src/services/completions.ts b/src/services/completions.ts index 40053d2e87350..27af7dc809bf4 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1183,9 +1183,9 @@ namespace ts.Completions { const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker(); const { moduleSymbol } = origin; - const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); + const targetSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction( - exportedSymbol, + targetSymbol, moduleSymbol, sourceFile, getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions)), diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 4d49f63117434..2a43faf326965 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -79,10 +79,14 @@ namespace ts { } const isDefault = exportKind === ExportKind.Default; const namedSymbol = isDefault && getLocalSymbolForExportDefault(symbol) || symbol; - // A re-export merged with an export from a module augmentation can result in `symbol` - // being an external module symbol; the name it is re-exported by will be `symbolTableKey` - // (which comes from the keys of `moduleSymbol.exports`.) - const importedName = isExternalModuleSymbol(namedSymbol) + // 1. A named export must be imported by its key in `moduleSymbol.exports` or `moduleSymbol.members`. + // 2. A re-export merged with an export from a module augmentation can result in `symbol` + // being an external module symbol; the name it is re-exported by will be `symbolTableKey` + // (which comes from the keys of `moduleSymbol.exports`.) + // 3. Otherwise, we have a default/namespace import that can be imported by any name, and + // `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to + // get a better name. + const importedName = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol) ? unescapeLeadingUnderscores(symbolTableKey) : getNameForExportedSymbol(namedSymbol, scriptTarget); const moduleName = stripQuotes(moduleSymbol.name); @@ -321,7 +325,7 @@ namespace ts { let moduleCount = 0; forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => { if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested(); - const seenExports = new Map(); + const seenExports = new Map<__String, true>(); const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); // Note: I think we shouldn't actually see resolved module symbols here, but weird merges @@ -339,7 +343,7 @@ namespace ts { checker); } checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { - if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, exported)) { + if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, key)) { cache.add( importingFile.path, exported, diff --git a/tests/cases/fourslash/server/completionsImport_jsModuleExportsAssignment.ts b/tests/cases/fourslash/server/completionsImport_jsModuleExportsAssignment.ts new file mode 100644 index 0000000000000..37345dcc3e9bd --- /dev/null +++ b/tests/cases/fourslash/server/completionsImport_jsModuleExportsAssignment.ts @@ -0,0 +1,57 @@ +/// + +// @Filename: /tsconfig.json +//// { "compilerOptions": { "module": "commonjs", "allowJs": true } } + +// @Filename: /third_party/marked/src/defaults.js +//// function getDefaults() { +//// return { +//// baseUrl: null, +//// }; +//// } +//// +//// function changeDefaults(newDefaults) { +//// module.exports.defaults = newDefaults; +//// } +//// +//// module.exports = { +//// defaults: getDefaults(), +//// getDefaults, +//// changeDefaults +//// }; + +// @Filename: /index.ts +//// /**/ + +format.setOption("newLineCharacter", "\n") +goTo.marker(""); + +// Create the exportInfoMap +verify.completions({ marker: "", preferences: { includeCompletionsForModuleExports: true } }); + +// Create a new program and reuse the exportInfoMap from the last request +edit.insert("d"); +verify.completions({ + marker: "", + excludes: ["newDefaults"], + includes: [{ + name: "defaults", + source: "/third_party/marked/src/defaults", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }], + preferences: { includeCompletionsForModuleExports: true } +}); + +verify.applyCodeActionFromCompletion("", { + name: "defaults", + source: "/third_party/marked/src/defaults", + description: `Import 'defaults' from module "./third_party/marked/src/defaults"`, + data: { + exportName: "defaults", + fileName: "/third_party/marked/src/defaults.js", + }, + newFileContent: `import { defaults } from "./third_party/marked/src/defaults"; + +d` +});