From 258be217a6244efcd8055d4096b50585427c5dce Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 4 Feb 2021 10:22:15 -0800 Subject: [PATCH] Fix completions crash on transient exported property named 'default' (#42583) * Fix completions crash on transient exported property named default * Revert simplification, both conditions were needed --- src/harness/fourslashImpl.ts | 2 +- src/services/codefixes/importFixes.ts | 12 +++++------ src/services/completions.ts | 2 +- src/services/utilities.ts | 6 +++--- ...completionsImport_weirdDefaultSynthesis.ts | 20 +++++++++++++++++++ 5 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 tests/cases/fourslash/completionsImport_weirdDefaultSynthesis.ts diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index aa708bfcf4c..8fd2f7f2447 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -2802,7 +2802,7 @@ namespace FourSlash { const matchingName = completions?.filter(e => e.name === options.name); const detailMessage = matchingName?.length ? `\n Found ${matchingName.length} with name '${options.name}' from source(s) ${matchingName.map(e => `'${e.source}'`).join(", ")}.` - : ""; + : ` (In fact, there were no completions with name '${options.name}' at all.)`; return this.raiseError(`No completions were found for the given name, source, and preferences.` + detailMessage); } const codeActions = details.codeActions; diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 628c26d294e..9700e6b583b 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -609,7 +609,7 @@ namespace ts.codefix { const exported = getDefaultLikeExportWorker(importingFile, moduleSymbol, checker, compilerOptions); if (!exported) return undefined; const { symbol, kind } = exported; - const info = getDefaultExportInfoWorker(symbol, moduleSymbol, checker, compilerOptions); + const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); return info && { symbol, kind, ...info }; } @@ -645,7 +645,7 @@ namespace ts.codefix { return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS; } - function getDefaultExportInfoWorker(defaultExport: Symbol, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined { + function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined { const localSymbol = getLocalSymbolForExportDefault(defaultExport); if (localSymbol) return { symbolForMeaning: localSymbol, name: localSymbol.name }; @@ -659,7 +659,7 @@ namespace ts.codefix { // 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, aliased.parent, checker, compilerOptions); + return getDefaultExportInfoWorker(aliased, checker, compilerOptions); } } @@ -667,15 +667,13 @@ namespace ts.codefix { defaultExport.escapedName !== InternalSymbolName.ExportEquals) { return { symbolForMeaning: defaultExport, name: defaultExport.getName() }; } - return { symbolForMeaning: defaultExport, name: moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) }; + return { symbolForMeaning: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) }; } function getNameForExportDefault(symbol: Symbol): string | undefined { return symbol.declarations && firstDefined(symbol.declarations, declaration => { if (isExportAssignment(declaration)) { - if (isIdentifier(declaration.expression)) { - return declaration.expression.text; - } + 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"); diff --git a/src/services/completions.ts b/src/services/completions.ts index a59f637a96e..a3c957442cb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -732,7 +732,7 @@ namespace ts.Completions { exportedSymbol, moduleSymbol, sourceFile, - getNameForExportedSymbol(symbol, compilerOptions.target!), + getNameForExportedSymbol(symbol, compilerOptions.target), host, program, formatContext, diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 8344c3e60ff..1fb8a23043e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2880,10 +2880,10 @@ namespace ts { return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray; } - export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget) { - if (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default) { + export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined) { + if (!(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default)) { // Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase. - return firstDefined(symbol.declarations, d => isExportAssignment(d) && isIdentifier(d.expression) ? d.expression.text : undefined) + return firstDefined(symbol.declarations, d => isExportAssignment(d) ? tryCast(skipOuterExpressions(d.expression), isIdentifier)?.text : undefined) || codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget); } return symbol.name; diff --git a/tests/cases/fourslash/completionsImport_weirdDefaultSynthesis.ts b/tests/cases/fourslash/completionsImport_weirdDefaultSynthesis.ts new file mode 100644 index 00000000000..b8d3558da3e --- /dev/null +++ b/tests/cases/fourslash/completionsImport_weirdDefaultSynthesis.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /collection.ts +//// class Collection { +//// public static readonly default: typeof Collection = Collection; +//// } +//// export = Collection as typeof Collection & { default: typeof Collection }; + +// @Filename: /index.ts +//// Colle/**/ + +verify.applyCodeActionFromCompletion("", { + name: "Collection", + source: "/collection", + description: `Import 'Collection' from module "./collection"`, + preferences: { + includeCompletionsForModuleExports: true + }, + newFileContent: `import Collection = require("./collection");\n\nColle` +});