diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 0bf0dc06c78..db2327654a3 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -231,11 +231,11 @@ namespace ts.codefix { function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { - return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson }; } const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol); if (named && skipAlias(named, checker) === symbol) { - return { symbol: named, moduleSymbol, moduleFileName: undefined, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: named, moduleSymbol, moduleFileName: undefined, exportKind: ExportKind.Named, targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson }; } } } @@ -256,12 +256,12 @@ namespace ts.codefix { const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) { - result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, isTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), 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)) { - result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, targetFlags: skipAlias(exported, checker).flags, isFromPackageJson }); } } }); @@ -294,10 +294,6 @@ namespace ts.codefix { return result && { ...result, computedWithoutCacheCount }; } - function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean { - return !(skipAlias(s, checker).flags & SymbolFlags.Value); - } - function isTypeOnlyPosition(sourceFile: SourceFile, position: number) { return isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); } @@ -395,9 +391,9 @@ namespace ts.codefix { }); } - function getExistingImportDeclarations({ moduleSymbol, exportKind, isTypeOnly: exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] { + function getExistingImportDeclarations({ moduleSymbol, exportKind, targetFlags }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] { // Can't use an es6 import for a type in JS. - if (exportedSymbolIsTypeOnly && isSourceFileJS(importingFile)) return emptyArray; + if (!(targetFlags & SymbolFlags.Value) && isSourceFileJS(importingFile)) return emptyArray; const importKind = getImportKind(importingFile, exportKind, compilerOptions); return mapDefined(importingFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => { const i = importFromModuleSpecifier(moduleSpecifier); @@ -462,7 +458,7 @@ namespace ts.codefix { computedWithoutCacheCount += computedWithoutCache ? 1 : 0; return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType => // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. - exportInfo.isTypeOnly && isJs && position !== undefined + !(exportInfo.targetFlags & SymbolFlags.Value) && isJs && position !== undefined ? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo } : { kind: ImportFixKind.AddNew, @@ -547,7 +543,7 @@ namespace ts.codefix { if (!umdSymbol) return undefined; const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; - const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, moduleFileName: undefined, exportKind: ExportKind.UMD, isTypeOnly: false, isFromPackageJson: false }]; + const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, moduleFileName: undefined, exportKind: ExportKind.UMD, targetFlags: symbol.flags, isFromPackageJson: false }]; const useRequire = shouldUseRequire(sourceFile, program); const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; @@ -653,7 +649,7 @@ namespace ts.codefix { !toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost) ) { const checker = program.getTypeChecker(); - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, moduleFileName: toFile?.fileName, exportKind, isTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, moduleFileName: toFile?.fileName, exportKind, targetFlags: skipAlias(exportedSymbol, checker).flags, isFromPackageJson }); } } forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => { diff --git a/src/services/completions.ts b/src/services/completions.ts index 7fe0647cb8f..5888f897e8b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1452,7 +1452,7 @@ namespace ts.Completions { const symbolToOriginInfoMap: SymbolOriginInfoMap = []; const symbolToSortTextIdMap: SymbolSortTextIdMap = []; const seenPropertySymbols = new Map(); - const isTypeOnly = isTypeOnlyCompletion(); + const isTypeOnlyLocation = isTypeOnlyCompletion(); const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); }); @@ -1506,7 +1506,7 @@ namespace ts.Completions { isJsxInitializer, insideJsDocTagTypeExpression, symbolToSortTextIdMap, - isTypeOnlyLocation: isTypeOnly, + isTypeOnlyLocation, isJsxIdentifierExpected, importCompletionNode, hasUnresolvedAutoImports, @@ -1806,7 +1806,7 @@ namespace ts.Completions { const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile; isInSnippetScope = isSnippetScope(scopeNode); - const symbolMeanings = (isTypeOnly ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias; + const symbolMeanings = (isTypeOnlyLocation ? SymbolFlags.None : SymbolFlags.Value) | SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Alias; symbols = concatenate(symbols, typeChecker.getSymbolsInScope(scopeNode, symbolMeanings)); Debug.assertEachIsDefined(symbols, "getSymbolsInScope() should all be defined"); @@ -1829,7 +1829,7 @@ namespace ts.Completions { } } collectAutoImports(); - if (isTypeOnly) { + if (isTypeOnlyLocation) { keywordFilters = contextToken && isAssertionExpression(contextToken.parent) ? KeywordCompletionFilters.TypeAssertionKeywords : KeywordCompletionFilters.TypeKeywords; @@ -1931,6 +1931,9 @@ namespace ts.Completions { context => { exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => { if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; + // `targetFlags` should be the same for each `info` + if (!isTypeOnlyLocation && !importCompletionNode && !(info[0].targetFlags & SymbolFlags.Value)) return; + if (isTypeOnlyLocation && !(info[0].targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return; const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || !detailsEntryId && charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) { const defaultExportInfo = find(info, isImportableExportInfo); diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index f13c9d6fc9b..4ff87083dff 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -20,8 +20,7 @@ namespace ts { /** Set if `moduleSymbol` is an external module, not an ambient module */ moduleFileName: string | undefined; exportKind: ExportKind; - /** If true, can't use an es6 import from a js file. */ - isTypeOnly: boolean; + targetFlags: SymbolFlags; /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ isFromPackageJson: boolean; } @@ -38,7 +37,7 @@ namespace ts { readonly moduleSymbol: Symbol | undefined; moduleFileName: string | undefined; exportKind: ExportKind; - isTypeOnly: boolean; + targetFlags: SymbolFlags; isFromPackageJson: boolean; } @@ -92,7 +91,7 @@ namespace ts { moduleFile, moduleFileName: moduleFile?.fileName, exportKind, - isTypeOnly: !(skipAlias(symbol, checker).flags & SymbolFlags.Value), + targetFlags: skipAlias(symbol, checker).flags, isFromPackageJson, symbol: storedSymbol, moduleSymbol: storedModuleSymbol, @@ -143,7 +142,7 @@ namespace ts { function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo { if (info.symbol && info.moduleSymbol) return info as SymbolExportInfo; - const { id, exportKind, isTypeOnly, isFromPackageJson, moduleFileName } = info; + const { id, exportKind, targetFlags, isFromPackageJson, moduleFileName } = info; const [cachedSymbol, cachedModuleSymbol] = symbols.get(id) || emptyArray; if (cachedSymbol && cachedModuleSymbol) { return { @@ -151,7 +150,7 @@ namespace ts { moduleSymbol: cachedModuleSymbol, moduleFileName, exportKind, - isTypeOnly, + targetFlags, isFromPackageJson, }; } @@ -173,7 +172,7 @@ namespace ts { moduleSymbol, moduleFileName, exportKind, - isTypeOnly, + targetFlags, isFromPackageJson, }; } diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index 6e08d5d3d46..de1a2c3eb34 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -2,6 +2,7 @@ // @Filename: /mod.ts //// export const foo = 0; +//// export type Foo = number; // @Filename: /index0.ts //// [|import f/*0*/|] @@ -32,6 +33,13 @@ isSnippet: true, replacementSpan: test.ranges()[marker], sourceDisplay: "./mod", + }, { + name: "Foo", + source: "./mod", + insertText: `import { Foo$1 } from "./mod";`, + isSnippet: true, + replacementSpan: test.ranges()[marker], + sourceDisplay: "./mod", }], preferences: { includeCompletionsForImportStatements: true,