diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ff905db8dbf54..c2fd1eed63601 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -531,7 +531,7 @@ namespace ts { return result; } - export function flatMapIterator(iter: Iterator, mapfn: (x: T) => U[] | Iterator | undefined): Iterator { + export function flatMapIterator(iter: Iterator, mapfn: (x: T) => ReadonlyArray | Iterator | undefined): Iterator { const first = iter.next(); if (first.done) { return emptyIterator; @@ -1418,7 +1418,9 @@ namespace ts { return typeof text === "string"; } - export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined { + export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; + export function tryCast(value: T, test: (value: T) => boolean): T | undefined; + export function tryCast(value: T, test: (value: T) => boolean): T | undefined { return value !== undefined && test(value) ? value : undefined; } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 540da3826b086..07efb849cc62d 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4470,5 +4470,9 @@ "Add missing enum member '{0}'": { "category": "Message", "code": 95063 + }, + "Add all missing imports": { + "category": "Message", + "code": 95064 } } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a1c6d5137b27d..8f3028c45c5f1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4312,8 +4312,8 @@ namespace ts { return !!forEachAncestorDirectory(directory, d => callback(d) ? true : undefined); } - export function isUMDExportSymbol(symbol: Symbol | undefined) { - return symbol && symbol.declarations && symbol.declarations[0] && isNamespaceExportDeclaration(symbol.declarations[0]); + export function isUMDExportSymbol(symbol: Symbol | undefined): boolean { + return !!symbol && !!symbol.declarations && !!symbol.declarations[0] && isNamespaceExportDeclaration(symbol.declarations[0]); } export function showModuleSpecifier({ moduleSpecifier }: ImportDeclaration): string { @@ -8105,4 +8105,6 @@ namespace ts { return findBestPatternMatch(patterns, _ => _, candidate); } + + export type Mutable = { -readonly [K in keyof T]: T[K] }; } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index f2ff965d4b2bd..68930fc96e599 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2631,7 +2631,7 @@ Actual: ${stringify(fullActual)}`); } const range = ts.firstOrUndefined(ranges); - const codeFixes = this.getCodeFixes(fileName, errorCode, preferences).filter(f => f.fixId === undefined); // TODO: GH#20315 filter out those that use the import fix ID; + const codeFixes = this.getCodeFixes(fileName, errorCode, preferences).filter(f => f.fixId === ts.codefix.importFixId); if (codeFixes.length === 0) { if (expectedTextArray.length !== 0) { diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index 58a3e3d1375d5..ad4e37974b3bc 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -1,9 +1,9 @@ /* @internal */ namespace ts { export interface CodeFixRegistration { - errorCodes: number[]; + errorCodes: ReadonlyArray; getCodeActions(context: CodeFixContext): CodeFixAction[] | undefined; - fixIds?: string[]; + fixIds?: ReadonlyArray; getAllCodeActions?(context: CodeFixAllContext): CombinedCodeActions; } @@ -27,7 +27,7 @@ namespace ts { const errorCodeToFixes = createMultiMap(); const fixIdToRegistration = createMap(); - type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string]; + export type DiagnosticAndArguments = DiagnosticMessage | [DiagnosticMessage, string] | [DiagnosticMessage, string, string]; function diagnosticToString(diag: DiagnosticAndArguments): string { return isArray(diag) ? formatStringFromArgs(getLocaleSpecificMessage(diag[0]), diag.slice(1) as ReadonlyArray) @@ -89,7 +89,7 @@ namespace ts { return createCombinedCodeActions(changes, commands.length === 0 ? undefined : commands); } - export function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: number[], cb: (diag: DiagnosticWithLocation) => void): void { + export function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: ReadonlyArray, cb: (diag: DiagnosticWithLocation) => void): void { for (const diag of program.getSemanticDiagnostics(sourceFile, cancellationToken).concat(computeSuggestionDiagnostics(sourceFile, program, cancellationToken))) { if (contains(errorCodes, diag.code)) { cb(diag as DiagnosticWithLocation); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index d2f1095ef4cca..8876a26cfa187 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1,58 +1,120 @@ /* @internal */ namespace ts.codefix { - import ChangeTracker = textChanges.ChangeTracker; + export const importFixId = "fixMissingImport"; + const errorCodes: ReadonlyArray = [ + Diagnostics.Cannot_find_name_0.code, + Diagnostics.Cannot_find_name_0_Did_you_mean_1.code, + Diagnostics.Cannot_find_namespace_0.code, + Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code, + Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here.code, + ]; registerCodeFix({ - errorCodes: [ - Diagnostics.Cannot_find_name_0.code, - Diagnostics.Cannot_find_name_0_Did_you_mean_1.code, - Diagnostics.Cannot_find_namespace_0.code, - Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code, - Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here.code, - ], - getCodeActions: context => context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code - ? getActionsForUMDImport(context) - : getActionsForNonUMDImport(context), - // TODO: GH#20315 - fixIds: [], - getAllCodeActions: notImplemented, - }); - - interface SymbolContext extends textChanges.TextChangesContext { - sourceFile: SourceFile; - symbolName: string; - } - - interface ImportCodeFixContext extends SymbolContext { - symbolToken: Node | undefined; - program: Program; - checker: TypeChecker; - compilerOptions: CompilerOptions; - getCanonicalFileName: GetCanonicalFileName; - preferences: UserPreferences; - } - - function createCodeAction(descriptionDiagnostic: DiagnosticMessage, diagnosticArgs: [string, string], changes: FileTextChanges[]): CodeFixAction { - // TODO: GH#20315 - return createCodeFixActionNoFixId("import", changes, [descriptionDiagnostic, ...diagnosticArgs] as [DiagnosticMessage, string, string]); - } + errorCodes, + getCodeActions(context) { + const { errorCode, preferences, sourceFile, span } = context; + const info = getFixesInfo(context, errorCode, span.start); + if (!info) return undefined; + const { fixes, symbolName } = info; + const quotePreference = getQuotePreference(sourceFile, preferences); + return fixes.map(fix => codeActionForFix(context, sourceFile, symbolName, fix, quotePreference)); + }, + fixIds: [importFixId], + getAllCodeActions: context => { + const { sourceFile, preferences } = context; + + // Namespace fixes don't conflict, so just build a list. + const addToNamespace: FixUseNamespaceImport[] = []; + // Keys are import clause node IDs. + const addToExisting = createMap<{ readonly importClause: ImportClause, defaultImport: string | undefined; readonly namedImports: string[] }>(); + // Keys are module specifiers. + const newImports = createMap>(); + + eachDiagnostic(context, errorCodes, diag => { + const info = getFixesInfo(context, diag.code, diag.start); + if (!info || !info.fixes.length) return; + const { fixes, symbolName } = info; + + const fix = first(fixes); + switch (fix.kind) { + case ImportFixKind.UseNamespace: + addToNamespace.push(fix); + break; + case ImportFixKind.AddToExisting: { + const { importClause, importKind } = fix; + const key = String(getNodeId(importClause)); + let entry = addToExisting.get(key); + if (!entry) { + addToExisting.set(key, entry = { importClause, defaultImport: undefined, namedImports: [] }); + } + if (importKind === ImportKind.Named) { + pushIfUnique(entry.namedImports, symbolName); + } + else { + Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName); + entry.defaultImport = symbolName; + } + break; + } + case ImportFixKind.AddNew: { + const { moduleSpecifier, importKind } = fix; + let entry = newImports.get(moduleSpecifier); + if (!entry) { + newImports.set(moduleSpecifier, entry = { defaultImport: undefined, namedImports: [], namespaceLikeImport: undefined }); + } + switch (importKind) { + case ImportKind.Default: + Debug.assert(entry.defaultImport === undefined || entry.defaultImport === symbolName); + entry.defaultImport = symbolName; + break; + case ImportKind.Named: + pushIfUnique(entry.namedImports, symbolName); + break; + case ImportKind.Equals: + case ImportKind.Namespace: + Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName); + entry.namespaceLikeImport = { importKind, name: symbolName }; + break; + } + break; + } + default: + Debug.assertNever(fix); + } + }); - function convertToImportCodeFixContext(context: CodeFixContext, symbolToken: Node, symbolName: string): ImportCodeFixContext { - const { program } = context; - const checker = program.getTypeChecker(); + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { + for (const fix of addToNamespace) { + addNamespaceQualifier(changes, sourceFile, fix); + } + addToExisting.forEach(({ importClause, defaultImport, namedImports }) => { + doAddExistingFix(changes, sourceFile, importClause, defaultImport, namedImports); + }); + const quotePreference = getQuotePreference(sourceFile, preferences); + newImports.forEach((imports, moduleSpecifier) => { + addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, imports); + }); + })); + }, + }); - return { - host: context.host, - formatContext: context.formatContext, - sourceFile: context.sourceFile, - program, - checker, - compilerOptions: program.getCompilerOptions(), - getCanonicalFileName: createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(context.host)), - symbolName, - symbolToken, - preferences: context.preferences, - }; + // Sorted with the preferred fix coming first. + const enum ImportFixKind { UseNamespace, AddToExisting, AddNew } + type ImportFix = FixUseNamespaceImport | FixAddToExistingImport | FixAddNewImport; + interface FixUseNamespaceImport { + readonly kind: ImportFixKind.UseNamespace; + readonly namespacePrefix: string; + readonly symbolToken: Identifier; + } + interface FixAddToExistingImport { + readonly kind: ImportFixKind.AddToExisting; + readonly importClause: ImportClause; + readonly importKind: ImportKind.Default | ImportKind.Named; + } + interface FixAddNewImport { + readonly kind: ImportFixKind.AddNew; + readonly moduleSpecifier: string; + readonly importKind: ImportKind; } const enum ImportKind { @@ -69,17 +131,11 @@ namespace ts.codefix { } /** Information needed to augment an existing import declaration. */ - interface ExistingImportInfo { + interface FixAddToExistingImportInfo { readonly declaration: AnyImportSyntax; readonly importKind: ImportKind; } - /** Information needed to create a new import declaration. */ - interface NewImportInfo { - readonly moduleSpecifier: string; - readonly importKind: ImportKind; - } - export function getImportCompletionAction( exportedSymbol: Symbol, moduleSymbol: Symbol, @@ -88,10 +144,8 @@ namespace ts.codefix { host: LanguageServiceHost, program: Program, checker: TypeChecker, - compilerOptions: CompilerOptions, allSourceFiles: ReadonlyArray, formatContext: formatting.FormatContext, - getCanonicalFileName: GetCanonicalFileName, symbolToken: Node | undefined, preferences: UserPreferences, ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { @@ -99,8 +153,8 @@ namespace ts.codefix { Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol)); // We sort the best codefixes first, so taking `first` is best for completions. const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, host, preferences)).moduleSpecifier; - const ctx: ImportCodeFixContext = { host, program, checker, compilerOptions, sourceFile, formatContext, symbolName, getCanonicalFileName, symbolToken, preferences }; - return { moduleSpecifier, codeAction: first(getCodeActionsForImport(exportInfos, ctx)) }; + const fix = first(getFixForImport(exportInfos, symbolName, symbolToken, program, sourceFile, host, preferences)); + return { moduleSpecifier, codeAction: codeActionForFix({ host, formatContext }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences)) }; } function getAllReExportingModules(exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, sourceFile: SourceFile, checker: TypeChecker, allSourceFiles: ReadonlyArray): ReadonlyArray { const result: SymbolExportInfo[] = []; @@ -120,26 +174,25 @@ namespace ts.codefix { return result; } - function getCodeActionsForImport(exportInfos: ReadonlyArray, context: ImportCodeFixContext): CodeFixAction[] { - const result: CodeFixAction[] = []; - getCodeActionsForImport_separateExistingAndNew(exportInfos, context, result, result); - return result; + function getFixForImport( + exportInfos: ReadonlyArray, + symbolName: string, + symbolToken: Node | undefined, + program: Program, + sourceFile: SourceFile, + host: LanguageServiceHost, + preferences: UserPreferences, + ): ReadonlyArray { + const checker = program.getTypeChecker(); + const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile)); + const useNamespace = tryUseExistingNamespaceImport(existingImports, symbolName, symbolToken, checker); + const addToExisting = tryAddToExistingImport(existingImports); + // Don't bother providing an action to add a new import if we can add to an existing one. + const addImport = addToExisting ? [addToExisting] : getCodeActionsForAddImport(exportInfos, existingImports, program, sourceFile, host, preferences); + return [...(useNamespace ? [useNamespace] : emptyArray), ...addImport]; } - function getCodeActionsForImport_separateExistingAndNew(exportInfos: ReadonlyArray, context: ImportCodeFixContext, useExisting: Push, addNew: Push): void { - const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, context.checker, context.sourceFile)); - - append(useExisting, tryUseExistingNamespaceImport(existingImports, context, context.symbolToken, context.checker)); - const addToExisting = tryAddToExistingImport(existingImports, context); - - if (addToExisting) { - useExisting.push(addToExisting); - } - else { // Don't bother providing an action to add a new import if we can add to an existing one. - getCodeActionsForAddImport(exportInfos, context, existingImports, addNew); - } - } - function tryUseExistingNamespaceImport(existingImports: ReadonlyArray, context: SymbolContext, symbolToken: Node | undefined, checker: TypeChecker): CodeFixAction | undefined { + function tryUseExistingNamespaceImport(existingImports: ReadonlyArray, symbolName: string, symbolToken: Node | undefined, checker: TypeChecker): FixUseNamespaceImport | undefined { // It is possible that multiple import statements with the same specifier exist in the file. // e.g. // @@ -152,25 +205,26 @@ namespace ts.codefix { // 1. change "member3" to "ns.member3" // 2. add "member3" to the second import statement's import list // and it is up to the user to decide which one fits best. - return !symbolToken || !isIdentifier(symbolToken) ? undefined : firstDefined(existingImports, ({ declaration }) => { + return !symbolToken || !isIdentifier(symbolToken) ? undefined : firstDefined(existingImports, ({ declaration }): FixUseNamespaceImport | undefined => { const namespace = getNamespaceImportName(declaration); if (namespace) { - const moduleSymbol = namespace && checker.getAliasedSymbol(checker.getSymbolAtLocation(namespace)!); - if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(context.symbolName))) { - return getCodeActionForUseExistingNamespaceImport(namespace.text, context, symbolToken); + const moduleSymbol = checker.getAliasedSymbol(checker.getSymbolAtLocation(namespace)!); + if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(symbolName))) { + return { kind: ImportFixKind.UseNamespace, namespacePrefix: namespace.text, symbolToken }; } } }); } - function tryAddToExistingImport(existingImports: ReadonlyArray, context: SymbolContext): CodeFixAction | undefined { - return firstDefined(existingImports, ({ declaration, importKind }) => { - if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) { - const changes = tryUpdateExistingImport(context, declaration.importClause, importKind); - if (changes) { - const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText()); - return createCodeAction(Diagnostics.Add_0_to_existing_import_declaration_from_1, [context.symbolName, moduleSpecifierWithoutQuotes], changes); - } - } + + function tryAddToExistingImport(existingImports: ReadonlyArray): FixAddToExistingImport | undefined { + return firstDefined(existingImports, ({ declaration, importKind }): FixAddToExistingImport | undefined => { + if (declaration.kind !== SyntaxKind.ImportDeclaration) return undefined; + const { importClause } = declaration; + if (!importClause) return undefined; + const { name, namedBindings } = importClause; + return importKind === ImportKind.Default && !name || importKind === ImportKind.Named && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports) + ? { kind: ImportFixKind.AddToExisting, importClause, importKind } + : undefined; }); } @@ -184,180 +238,82 @@ namespace ts.codefix { } } - function getExistingImportDeclarations({ moduleSymbol, importKind }: SymbolExportInfo, checker: TypeChecker, { imports }: SourceFile): ReadonlyArray { - return mapDefined(imports, moduleSpecifier => { + function getExistingImportDeclarations({ moduleSymbol, importKind }: SymbolExportInfo, checker: TypeChecker, { imports }: SourceFile): ReadonlyArray { + return mapDefined(imports, moduleSpecifier => { const i = importFromModuleSpecifier(moduleSpecifier); return (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration) && checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind } : undefined; }); } - function getCodeActionForNewImport(context: SymbolContext & { preferences: UserPreferences }, { moduleSpecifier, importKind }: NewImportInfo): CodeFixAction { - const { sourceFile, symbolName, preferences } = context; - - const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); - const quotedModuleSpecifier = makeStringLiteral(moduleSpecifierWithoutQuotes, getQuotePreference(sourceFile, preferences)); - const importDecl = importKind !== ImportKind.Equals - ? createImportDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createImportClauseOfKind(importKind, symbolName), - quotedModuleSpecifier) - : createImportEqualsDeclaration( - /*decorators*/ undefined, - /*modifiers*/ undefined, - createIdentifier(symbolName), - createExternalModuleReference(quotedModuleSpecifier)); - - const changes = ChangeTracker.with(context, t => insertImport(t, sourceFile, importDecl)); - - // if this file doesn't have any import statements, insert an import statement and then insert a new line - // between the only import statement and user code. Otherwise just insert the statement because chances - // are there are already a new line separating code and import statements. - return createCodeAction(Diagnostics.Import_0_from_module_1, [symbolName, moduleSpecifierWithoutQuotes], changes); - } - - function createImportClauseOfKind(kind: ImportKind.Default | ImportKind.Named | ImportKind.Namespace, symbolName: string) { - const id = createIdentifier(symbolName); - switch (kind) { - case ImportKind.Default: - return createImportClause(id, /*namedBindings*/ undefined); - case ImportKind.Namespace: - return createImportClause(/*name*/ undefined, createNamespaceImport(id)); - case ImportKind.Named: - return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, id)])); - default: - Debug.assertNever(kind); - } - } - function getNewImportInfos( program: Program, sourceFile: SourceFile, moduleSymbols: ReadonlyArray, host: LanguageServiceHost, preferences: UserPreferences, - ): ReadonlyArray { - const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind }) => { + ): ReadonlyArray { + const choicesForEachExportingModule = flatMap>(moduleSymbols, ({ moduleSymbol, importKind }) => { const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences); - return modulePathsGroups.map(group => group.map(moduleSpecifier => ({ moduleSpecifier, importKind }))); + return modulePathsGroups.map(group => group.map((moduleSpecifier): FixAddNewImport => ({ kind: ImportFixKind.AddNew, moduleSpecifier, importKind }))); }); // Sort to keep the shortest paths first, but keep [relativePath, importRelativeToBaseUrl] groups together - return flatten(choicesForEachExportingModule.sort((a, b) => first(a).moduleSpecifier.length - first(b).moduleSpecifier.length)); + return flatten(choicesForEachExportingModule.sort((a, b) => first(a).moduleSpecifier.length - first(b).moduleSpecifier.length)); } function getCodeActionsForAddImport( exportInfos: ReadonlyArray, - ctx: ImportCodeFixContext, - existingImports: ReadonlyArray, - addNew: Push, - ): void { + existingImports: ReadonlyArray, + program: Program, + sourceFile: SourceFile, + host: LanguageServiceHost, + preferences: UserPreferences, + ): ReadonlyArray { const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier); - const newImportInfos = existingDeclaration - ? [existingDeclaration] - : getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.host, ctx.preferences); - for (const info of newImportInfos) { - addNew.push(getCodeActionForNewImport(ctx, info)); - } + return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, exportInfos, host, preferences); } - function newImportInfoFromExistingSpecifier({ declaration, importKind }: ExistingImportInfo): NewImportInfo | undefined { + function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo): FixAddNewImport | undefined { const expression = declaration.kind === SyntaxKind.ImportDeclaration ? declaration.moduleSpecifier : declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference ? declaration.moduleReference.expression : undefined; - return expression && isStringLiteral(expression) ? { moduleSpecifier: expression.text, importKind } : undefined; - } - - function tryUpdateExistingImport(context: SymbolContext, importClause: ImportClause | ImportEqualsDeclaration, importKind: ImportKind): FileTextChanges[] | undefined { - const { symbolName, sourceFile } = context; - const { name } = importClause; - const { namedBindings } = (importClause.kind !== SyntaxKind.ImportEqualsDeclaration && importClause) as ImportClause; // TODO: GH#18217 - switch (importKind) { - case ImportKind.Default: - return name ? undefined : ChangeTracker.with(context, t => - t.replaceNode(sourceFile, importClause, createImportClause(createIdentifier(symbolName), namedBindings))); - - case ImportKind.Named: { - const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName)); - if (namedBindings && namedBindings.kind === SyntaxKind.NamedImports && namedBindings.elements.length !== 0) { - // There are already named imports; add another. - return ChangeTracker.with(context, t => t.insertNodeInListAfter( - sourceFile, - namedBindings.elements[namedBindings.elements.length - 1], - newImportSpecifier)); - } - if (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports && namedBindings.elements.length === 0) { - return ChangeTracker.with(context, t => - t.replaceNode(sourceFile, importClause, createImportClause(name, createNamedImports([newImportSpecifier])))); - } - return undefined; - } - - case ImportKind.Namespace: - return namedBindings ? undefined : ChangeTracker.with(context, t => - t.replaceNode(sourceFile, importClause, createImportClause(name, createNamespaceImport(createIdentifier(symbolName))))); - - case ImportKind.Equals: - return undefined; - - default: - Debug.assertNever(importKind); - } - } - - function getCodeActionForUseExistingNamespaceImport(namespacePrefix: string, context: SymbolContext, symbolToken: Identifier): CodeFixAction { - const { symbolName, sourceFile } = context; - - /** - * Cases: - * import * as ns from "mod" - * import default, * as ns from "mod" - * import ns = require("mod") - * - * Because there is no import list, we alter the reference to include the - * namespace instead of altering the import declaration. For example, "foo" would - * become "ns.foo" - */ - const changes = ChangeTracker.with(context, tracker => - tracker.replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), symbolToken))); - return createCodeAction(Diagnostics.Change_0_to_1, [symbolName, `${namespacePrefix}.${symbolName}`], changes); + return expression && isStringLiteral(expression) ? { kind: ImportFixKind.AddNew, moduleSpecifier: expression.text, importKind } : undefined; } - function getActionsForUMDImport(context: CodeFixContext): CodeFixAction[] | undefined { - const token = getTokenAtPosition(context.sourceFile, context.span.start); - const checker = context.program.getTypeChecker(); - - let umdSymbol: Symbol | undefined; - - if (isIdentifier(token)) { - // try the identifier to see if it is the umd symbol - umdSymbol = checker.getSymbolAtLocation(token); - } - - if (!isUMDExportSymbol(umdSymbol)) { - // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. - const parent = token.parent; - const isNodeOpeningLikeElement = isJsxOpeningLikeElement(parent); - if ((isJsxOpeningLikeElement && (parent).tagName === token) || parent.kind === SyntaxKind.JsxOpeningFragment) { - umdSymbol = checker.resolveName(checker.getJsxNamespace(parent), - isNodeOpeningLikeElement ? (parent).tagName : parent, SymbolFlags.Value, /*excludeGlobals*/ false); - } - } - - if (isUMDExportSymbol(umdSymbol)) { - const symbol = checker.getAliasedSymbol(umdSymbol!); - if (symbol) { - return getCodeActionsForImport([{ moduleSymbol: symbol, importKind: getUmdImportKind(context.program.getCompilerOptions()) }], - convertToImportCodeFixContext(context, token, umdSymbol!.name)); - } - } - - return undefined; + interface FixesInfo { readonly fixes: ReadonlyArray; readonly symbolName: string; } + function getFixesInfo(context: CodeFixContextBase, errorCode: number, pos: number): FixesInfo | undefined { + const symbolToken = getTokenAtPosition(context.sourceFile, pos); + const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code + ? getFixesInfoForUMDImport(context, symbolToken) + : getFixesInfoForNonUMDImport(context, symbolToken); + return info && { ...info, fixes: sort(info.fixes, (a, b) => a.kind - b.kind) }; } - function getUmdImportKind(compilerOptions: CompilerOptions) { + function getFixesInfoForUMDImport({ sourceFile, program, host, preferences }: CodeFixContextBase, token: Node): FixesInfo | undefined { + const checker = program.getTypeChecker(); + const umdSymbol = getUmdSymbol(token, checker); + if (!umdSymbol) return undefined; + const symbol = checker.getAliasedSymbol(umdSymbol); + const symbolName = umdSymbol.name; + const exportInfos: ReadonlyArray = [{ moduleSymbol: symbol, importKind: getUmdImportKind(program.getCompilerOptions()) }]; + const fixes = getFixForImport(exportInfos, symbolName, token, program, sourceFile, host, preferences); + return { fixes, symbolName }; + } + function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { + // try the identifier to see if it is the umd symbol + const umdSymbol = isIdentifier(token) ? checker.getSymbolAtLocation(token) : undefined; + if (isUMDExportSymbol(umdSymbol)) return umdSymbol; + + // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. + const { parent } = token; + return (isJsxOpeningLikeElement(parent) && parent.tagName === token) || isJsxOpeningFragment(parent) + ? tryCast(checker.resolveName(checker.getJsxNamespace(parent), isJsxOpeningLikeElement(parent) ? token : parent, SymbolFlags.Value, /*excludeGlobals*/ false), isUMDExportSymbol) + : undefined; + } + + function getUmdImportKind(compilerOptions: CompilerOptions): ImportKind { // Import a synthetic `default` if enabled. if (getAllowSyntheticDefaultImports(compilerOptions)) { return ImportKind.Default; @@ -381,11 +337,9 @@ namespace ts.codefix { } } - function getActionsForNonUMDImport(context: CodeFixContext): CodeFixAction[] | undefined { + function getFixesInfoForNonUMDImport({ sourceFile, program, cancellationToken, host, preferences }: CodeFixContextBase, symbolToken: Node): FixesInfo | undefined { // This will always be an Identifier, since the diagnostics we fix only fail on identifiers. - const { sourceFile, span, program, cancellationToken } = context; const checker = program.getTypeChecker(); - const symbolToken = getTokenAtPosition(sourceFile, span.start); // If we're at ``, we must check if `Foo` is already in scope, and if so, get an import for `React` instead. const symbolName = isJsxOpeningLikeElement(symbolToken.parent) && symbolToken.parent.tagName === symbolToken @@ -394,17 +348,22 @@ namespace ts.codefix { : isIdentifier(symbolToken) ? symbolToken.text : undefined; if (!symbolName) return undefined; // "default" is a keyword and not a legal identifier for the import, so we don't expect it here - Debug.assert(symbolName !== "default"); + Debug.assert(symbolName !== InternalSymbolName.Default); - const addToExistingDeclaration: CodeFixAction[] = []; - const addNewDeclaration: CodeFixAction[] = []; - getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).forEach(exportInfos => { - getCodeActionsForImport_separateExistingAndNew(exportInfos, convertToImportCodeFixContext(context, symbolToken, symbolName), addToExistingDeclaration, addNewDeclaration); - }); - return [...addToExistingDeclaration, ...addNewDeclaration]; + const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).entries(), ([_, exportInfos]) => + getFixForImport(exportInfos, symbolName, symbolToken, program, sourceFile, host, preferences))); + return { fixes, symbolName }; } - function getExportInfos(symbolName: string, currentTokenMeaning: SemanticMeaning, cancellationToken: CancellationToken, sourceFile: SourceFile, checker: TypeChecker, program: Program): ReadonlyMap> { + // Returns a map from an exported symbol's ID to a list of every way it's (re-)exported. + function getExportInfos( + symbolName: string, + currentTokenMeaning: SemanticMeaning, + cancellationToken: CancellationToken, + sourceFile: SourceFile, + checker: TypeChecker, + program: Program, + ): ReadonlyMap> { // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once. // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); @@ -422,14 +381,14 @@ namespace ts.codefix { localSymbol && localSymbol.escapedName === symbolName || getEscapedNameForExportDefault(defaultExport) === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, program.getCompilerOptions().target!) === symbolName - ) && checkSymbolHasMeaning(localSymbol || defaultExport, currentTokenMeaning)) { + ) && symbolHasMeaning(localSymbol || defaultExport, currentTokenMeaning)) { addSymbol(moduleSymbol, localSymbol || defaultExport, ImportKind.Default); } } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); - if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { + if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named); } @@ -450,7 +409,88 @@ namespace ts.codefix { return originalSymbolToExportInfos; } - function checkSymbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean { + function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference): CodeFixAction { + let diag!: DiagnosticAndArguments; + const changes = textChanges.ChangeTracker.with(context, tracker => { + diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, quotePreference); + }); + return createCodeFixAction("import", changes, diag, importFixId, Diagnostics.Add_all_missing_imports); + } + function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference): DiagnosticAndArguments { + switch (fix.kind) { + case ImportFixKind.UseNamespace: + addNamespaceQualifier(changes, sourceFile, fix); + return [Diagnostics.Change_0_to_1, symbolName, `${fix.namespacePrefix}.${symbolName}`]; + case ImportFixKind.AddToExisting: { + const { importClause, importKind } = fix; + doAddExistingFix(changes, sourceFile, importClause, importKind === ImportKind.Default ? symbolName : undefined, importKind === ImportKind.Named ? [symbolName] : emptyArray); + const moduleSpecifierWithoutQuotes = stripQuotes(importClause.parent.moduleSpecifier.getText()); + return [Diagnostics.Add_0_to_existing_import_declaration_from_1, symbolName, moduleSpecifierWithoutQuotes]; + } + case ImportFixKind.AddNew: { + const { importKind, moduleSpecifier } = fix; + addNewImports(changes, sourceFile, moduleSpecifier, quotePreference, importKind === ImportKind.Default ? { defaultImport: symbolName, namedImports: emptyArray, namespaceLikeImport: undefined } + : importKind === ImportKind.Named ? { defaultImport: undefined, namedImports: [symbolName], namespaceLikeImport: undefined } + : { defaultImport: undefined, namedImports: emptyArray, namespaceLikeImport: { importKind, name: symbolName } }); + return [Diagnostics.Import_0_from_module_1, symbolName, moduleSpecifier]; + } + default: + return Debug.assertNever(fix); + } + } + + function doAddExistingFix(changes: textChanges.ChangeTracker, sourceFile: SourceFile, clause: ImportClause, defaultImport: string | undefined, namedImports: ReadonlyArray): void { + if (defaultImport) { + Debug.assert(!clause.name); + changes.insertNodeAt(sourceFile, clause.getStart(sourceFile), createIdentifier(defaultImport), { suffix: ", " }); + } + + if (namedImports.length) { + const specifiers = namedImports.map(name => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name))); + if (clause.namedBindings && cast(clause.namedBindings, isNamedImports).elements.length) { + for (const spec of specifiers) { + changes.insertNodeInListAfter(sourceFile, last(cast(clause.namedBindings, isNamedImports).elements), spec); + } + } + else { + if (specifiers.length) { + const namedImports = createNamedImports(specifiers); + if (clause.namedBindings) { + changes.replaceNode(sourceFile, clause.namedBindings, namedImports); + } + else { + changes.insertNodeAfter(sourceFile, Debug.assertDefined(clause.name), namedImports); + } + } + } + } + } + + function addNamespaceQualifier(changes: textChanges.ChangeTracker, sourceFile: SourceFile, { namespacePrefix, symbolToken }: FixUseNamespaceImport): void { + changes.replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), symbolToken)); + } + + interface ImportsCollection { + readonly defaultImport: string | undefined; + readonly namedImports: string[]; + readonly namespaceLikeImport: { readonly importKind: ImportKind.Equals | ImportKind.Namespace, readonly name: string } | undefined; + } + function addNewImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleSpecifier: string, quotePreference: QuotePreference, { defaultImport, namedImports, namespaceLikeImport }: ImportsCollection): void { + const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference); + if (defaultImport !== undefined || namedImports.length) { + insertImport(changes, sourceFile, + makeImport( + defaultImport === undefined ? undefined : createIdentifier(defaultImport), + namedImports.map(n => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(n))), moduleSpecifier, quotePreference)); + } + if (namespaceLikeImport) { + insertImport(changes, sourceFile, namespaceLikeImport.importKind === ImportKind.Equals + ? createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createIdentifier(namespaceLikeImport.name), createExternalModuleReference(quotedModuleSpecifier)) + : createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(namespaceLikeImport.name))), quotedModuleSpecifier)); + } + } + + function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean { return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 535f6f4f745e4..40eedfb61a1c6 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -549,7 +549,6 @@ namespace ts.Completions { entryId: CompletionEntryIdentifier, host: LanguageServiceHost, formatContext: formatting.FormatContext, - getCanonicalFileName: GetCanonicalFileName, preferences: UserPreferences, cancellationToken: CancellationToken, ): CompletionEntryDetails | undefined { @@ -583,7 +582,7 @@ namespace ts.Completions { } case "symbol": { const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences); + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, program.getSourceFiles(), preferences); return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } case "literal": { @@ -645,7 +644,6 @@ namespace ts.Completions { sourceFile: SourceFile, previousToken: Node | undefined, formatContext: formatting.FormatContext, - getCanonicalFileName: GetCanonicalFileName, allSourceFiles: ReadonlyArray, preferences: UserPreferences, ): CodeActionsAndSourceDisplay { @@ -664,10 +662,8 @@ namespace ts.Completions { host, program, checker, - compilerOptions, allSourceFiles, formatContext, - getCanonicalFileName, previousToken, preferences); return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] }; diff --git a/src/services/services.ts b/src/services/services.ts index b2a2fdaaae21e..52afe6bd09c2d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1424,7 +1424,6 @@ namespace ts { { name, source }, host, (formattingOptions && formatting.getFormatContext(formattingOptions))!, // TODO: GH#18217 - getCanonicalFileName, preferences, cancellationToken, ); diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 8f00b6a7b5006..c0dabef82e77b 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -290,7 +290,7 @@ namespace ts.textChanges { return this.replaceNode(sourceFile, oldNode, newNode, { suffix }); } - private insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}) { + public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}) { this.replaceRange(sourceFile, createTextRange(pos), newNode, options); } @@ -478,6 +478,7 @@ namespace ts.textChanges { case SyntaxKind.VariableDeclaration: case SyntaxKind.StringLiteral: + case SyntaxKind.Identifier: return { prefix: ", " }; case SyntaxKind.PropertyAssignment: diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 044f56da8eaa3..b77eaf4c55947 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -132,7 +132,7 @@ declare namespace ts { */ function flatMap(array: ReadonlyArray, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[]; function flatMap(array: ReadonlyArray | undefined, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[] | undefined; - function flatMapIterator(iter: Iterator, mapfn: (x: T) => U[] | Iterator | undefined): Iterator; + function flatMapIterator(iter: Iterator, mapfn: (x: T) => ReadonlyArray | Iterator | undefined): Iterator; /** * Maps an array. If the mapped value is an array, it is spread into the result. * Avoids allocation if all elements map to themselves. @@ -347,6 +347,7 @@ declare namespace ts { */ function isString(text: any): text is string; function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; + function tryCast(value: T, test: (value: T) => boolean): T | undefined; function cast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut; /** Does nothing. */ function noop(_?: {} | null | undefined): void; @@ -5932,6 +5933,7 @@ declare namespace ts { Convert_default_export_to_named_export: DiagnosticMessage; Convert_named_export_to_default_export: DiagnosticMessage; Add_missing_enum_member_0: DiagnosticMessage; + Add_all_missing_imports: DiagnosticMessage; }; } declare namespace ts { @@ -6614,7 +6616,7 @@ declare namespace ts { function getObjectFlags(type: Type): ObjectFlags; function typeHasCallOrConstructSignatures(type: Type, checker: TypeChecker): boolean; function forSomeAncestorDirectory(directory: string, callback: (directory: string) => boolean): boolean; - function isUMDExportSymbol(symbol: Symbol | undefined): boolean | undefined; + function isUMDExportSymbol(symbol: Symbol | undefined): boolean; function showModuleSpecifier({ moduleSpecifier }: ImportDeclaration): string; function getLastChild(node: Node): Node | undefined; /** Add a value to a set, and return true if it wasn't already present. */ @@ -7378,6 +7380,9 @@ declare namespace ts { * (These are verified by verifyCompilerOptions to have 0 or 1 "*" characters.) */ function matchPatternOrExact(patternStrings: ReadonlyArray, candidate: string): string | Pattern | undefined; + type Mutable = { + -readonly [K in keyof T]: T[K]; + }; } declare namespace ts { function createNode(kind: SyntaxKind, pos?: number, end?: number): Node; @@ -10932,7 +10937,7 @@ declare namespace ts.Completions { name: string; source?: string; } - function getCompletionEntryDetails(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, host: LanguageServiceHost, formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, preferences: UserPreferences, cancellationToken: CancellationToken): CompletionEntryDetails | undefined; + function getCompletionEntryDetails(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences, cancellationToken: CancellationToken): CompletionEntryDetails | undefined; function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined; } declare namespace ts.DocumentHighlights { @@ -11524,7 +11529,7 @@ declare namespace ts.textChanges { replaceNodeRangeWithNodes(sourceFile: SourceFile, startNode: Node, endNode: Node, newNodes: ReadonlyArray, options?: ReplaceWithMultipleNodesOptions & ConfigurableStartEnd): this; private nextCommaToken; replacePropertyAssignment(sourceFile: SourceFile, oldNode: PropertyAssignment, newNode: PropertyAssignment): this; - private insertNodeAt; + insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options?: InsertNodeOptions): void; private insertNodesAt; insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void; insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween?: boolean): void; @@ -11576,9 +11581,9 @@ declare namespace ts.textChanges { } declare namespace ts { interface CodeFixRegistration { - errorCodes: number[]; + errorCodes: ReadonlyArray; getCodeActions(context: CodeFixContext): CodeFixAction[] | undefined; - fixIds?: string[]; + fixIds?: ReadonlyArray; getAllCodeActions?(context: CodeFixAllContext): CombinedCodeActions; } interface CodeFixContextBase extends textChanges.TextChangesContext { @@ -11605,7 +11610,7 @@ declare namespace ts { function createCombinedCodeActions(changes: FileTextChanges[], commands?: CodeActionCommand[]): CombinedCodeActions; function createFileTextChanges(fileName: string, textChanges: TextChange[]): FileTextChanges; function codeFixAll(context: CodeFixAllContext, errorCodes: number[], use: (changes: textChanges.ChangeTracker, error: DiagnosticWithLocation, commands: Push) => void): CombinedCodeActions; - function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: number[], cb: (diag: DiagnosticWithLocation) => void): void; + function eachDiagnostic({ program, sourceFile, cancellationToken }: CodeFixAllContext, errorCodes: ReadonlyArray, cb: (diag: DiagnosticWithLocation) => void): void; } } declare namespace ts { @@ -11646,7 +11651,8 @@ declare namespace ts.codefix { declare namespace ts.codefix { } declare namespace ts.codefix { - function getImportCompletionAction(exportedSymbol: Symbol, moduleSymbol: Symbol, sourceFile: SourceFile, symbolName: string, host: LanguageServiceHost, program: Program, checker: TypeChecker, compilerOptions: CompilerOptions, allSourceFiles: ReadonlyArray, formatContext: formatting.FormatContext, getCanonicalFileName: GetCanonicalFileName, symbolToken: Node | undefined, preferences: UserPreferences): { + const importFixId = "fixMissingImport"; + function getImportCompletionAction(exportedSymbol: Symbol, moduleSymbol: Symbol, sourceFile: SourceFile, symbolName: string, host: LanguageServiceHost, program: Program, checker: TypeChecker, allSourceFiles: ReadonlyArray, formatContext: formatting.FormatContext, symbolToken: Node | undefined, preferences: UserPreferences): { readonly moduleSpecifier: string; readonly codeAction: CodeAction; }; diff --git a/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts index b9ff183f3f63f..11a0bc2898561 100644 --- a/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts +++ b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts @@ -9,7 +9,7 @@ goTo.marker(""); verify.completionListContains({ name: "foo", source: "/a" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true, { - includeExternalModuleExports: true, + includeCompletionsForModuleExports: true, sourceDisplay: "./a", }); diff --git a/tests/cases/fourslash/importNameCodeFix_all.ts b/tests/cases/fourslash/importNameCodeFix_all.ts new file mode 100644 index 0000000000000..4aa22413d5a1f --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_all.ts @@ -0,0 +1,51 @@ +/// + +// @Filename: /a.ts +////export default function ad() {} +////export const a0 = 0; + +// @Filename: /b.ts +////export default function bd() {} +////export const b0 = 0; + +// @Filename: /c.ts +////export default function cd() {} +////export const c0 = 0; + +// @Filename: /d.ts +////export default function dd() {} +////export const d0 = 0; +////export const d1 = 1; + +// @Filename: /e.d.ts +////declare function e(): void; +////export = e; + +// @Filename: /user.ts +////import * as b from "./b"; +////import { } from "./c"; +////import dd from "./d"; +//// +////ad; ad; a0; a0; +////bd; bd; b0; b0; +////cd; cd; c0; c0; +////dd; dd; d0; d0; d1; d1; +////e; e; + +goTo.file("/user.ts"); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +// TODO: GH#25135 (should import 'e') +`import bd, * as b from "./b"; +import cd, { c0 } from "./c"; +import dd, { d0, d1 } from "./d"; +import ad, { a0 } from "./a"; + +ad; ad; a0; a0; +bd; bd; b.b0; b.b0; +cd; cd; c0; c0; +dd; dd; d0; d0; d1; d1; +e; e;`, +});