diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 40261c7e8e3..cff212dcaeb 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -32,6 +32,7 @@ import { flatten, forEach, forEachAncestorDirectory, + FutureSourceFile, getBaseFileName, GetCanonicalFileName, getConditions, @@ -65,6 +66,7 @@ import { isDeclarationFileName, isExternalModuleAugmentation, isExternalModuleNameRelative, + isFullSourceFile, isMissingPackageJsonInfo, isModuleBlock, isModuleDeclaration, @@ -141,7 +143,7 @@ export interface ModuleSpecifierPreferences { export function getModuleSpecifierPreferences( { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: Pick, oldImportSpecifier?: string, ): ModuleSpecifierPreferences { const filePreferredEnding = getPreferredEnding(); @@ -197,7 +199,7 @@ export function getModuleSpecifierPreferences( importModuleSpecifierEnding, resolutionMode ?? importingSourceFile.impliedNodeFormat, compilerOptions, - importingSourceFile, + isFullSourceFile(importingSourceFile) ? importingSourceFile : undefined, ); } } @@ -230,7 +232,7 @@ export function updateModuleSpecifier( /** @internal */ export function getModuleSpecifier( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, importingSourceFileName: string, toFileName: string, host: ModuleSpecifierResolutionHost, @@ -242,7 +244,7 @@ export function getModuleSpecifier( /** @internal */ export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, @@ -255,7 +257,7 @@ export function getNodeModulesPackageName( function getModuleSpecifierWorker( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, importingSourceFileName: string, toFileName: string, host: ModuleSpecifierResolutionHost, @@ -272,7 +274,7 @@ function getModuleSpecifierWorker( /** @internal */ export function tryGetModuleSpecifiersFromCache( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -288,7 +290,7 @@ export function tryGetModuleSpecifiersFromCache( function tryGetModuleSpecifiersFromCacheWorker( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -334,7 +336,7 @@ export function getModuleSpecifiersWithCacheInfo( moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -370,10 +372,30 @@ export function getModuleSpecifiersWithCacheInfo( return { moduleSpecifiers: result, computedWithoutCache }; } +/** @internal */ +export function getLocalModuleSpecifierBetweenFileNames( + importingFile: Pick, + targetFileName: string, + compilerOptions: CompilerOptions, + host: ModuleSpecifierResolutionHost, + options: ModuleSpecifierOptions = {}, +): string { + const info = getInfo(importingFile.fileName, host); + const importMode = options.overrideImportMode ?? importingFile.impliedNodeFormat; + return getLocalModuleSpecifier( + targetFileName, + info, + compilerOptions, + host, + importMode, + getModuleSpecifierPreferences({}, compilerOptions, importingFile), + ); +} + function computeModuleSpecifiers( modulePaths: readonly ModulePath[], compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -381,7 +403,7 @@ function computeModuleSpecifiers( ): readonly string[] { const info = getInfo(importingSourceFile.fileName, host); const preferences = getModuleSpecifierPreferences(userPreferences, compilerOptions, importingSourceFile); - const existingSpecifier = forEach(modulePaths, modulePath => + const existingSpecifier = isFullSourceFile(importingSourceFile) && forEach(modulePaths, modulePath => forEach( host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { @@ -1014,7 +1036,7 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam return processEnding(shortest, allowedEndings, compilerOptions); } -function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, canonicalSourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { +function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, canonicalSourceDirectory }: Info, importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 54c65f2e0c7..fead20508c5 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4217,6 +4217,18 @@ export interface SourceFileLike { getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } +/** @internal */ +export interface FutureSourceFile { + readonly path: Path; + readonly fileName: string; + readonly impliedNodeFormat?: ResolutionMode; + readonly packageJsonScope?: PackageJsonInfo; + readonly externalModuleIndicator?: true | undefined; + readonly commonJsModuleIndicator?: true | undefined; + readonly statements: readonly never[]; + readonly imports: readonly never[]; +} + /** @internal */ export interface RedirectInfo { /** Source file this redirects to. */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 7d3ff8712ab..8b862d2f838 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -11,6 +11,7 @@ import { AmpersandAmpersandEqualsToken, AnyImportOrBareOrAccessedRequire, AnyImportOrReExport, + AnyImportOrRequireStatement, AnyImportSyntax, AnyValidImportOrReExport, append, @@ -1985,6 +1986,11 @@ export function isAnyImportOrBareOrAccessedRequire(node: Node): node is AnyImpor return isAnyImportSyntax(node) || isVariableDeclarationInitializedToBareOrAccessedRequire(node); } +/** @internal */ +export function isAnyImportOrRequireStatement(node: Node): node is AnyImportOrRequireStatement { + return isAnyImportSyntax(node) || isRequireVariableStatement(node); +} + /** @internal */ export function isLateVisibilityPaintedStatement(node: Node): node is LateVisibilityPaintedStatement { switch (node.kind) { @@ -3461,6 +3467,11 @@ export function isInternalModuleImportEqualsDeclaration(node: Node): node is Imp return node.kind === SyntaxKind.ImportEqualsDeclaration && (node as ImportEqualsDeclaration).moduleReference.kind !== SyntaxKind.ExternalModuleReference; } +/** @internal */ +export function isFullSourceFile(sourceFile: object): sourceFile is SourceFile { + return (sourceFile as Partial)?.kind === SyntaxKind.SourceFile; +} + /** @internal */ export function isSourceFileJS(file: SourceFile): boolean { return isInJSFile(file); @@ -9576,7 +9587,7 @@ export function usesExtensionsOnImports({ imports }: SourceFile, hasExtension: ( } /** @internal */ -export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile): ModuleSpecifierEnding { +export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile?: SourceFile): ModuleSpecifierEnding { const moduleResolution = getEmitModuleResolutionKind(compilerOptions); const moduleResolutionIsNodeNext = ModuleResolutionKind.Node16 <= moduleResolution && moduleResolution <= ModuleResolutionKind.NodeNext; if (preference === "js" || resolutionMode === ModuleKind.ESNext && moduleResolutionIsNodeNext) { @@ -9603,22 +9614,22 @@ export function getModuleSpecifierEndingPreference(preference: UserPreferences[" // accurately, and more importantly, literally nobody wants `Index` and its existence is a mystery. if (!shouldAllowImportingTsExtension(compilerOptions)) { // If .ts imports are not valid, we only need to see one .js import to go with that. - return usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; + return sourceFile && usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } return inferPreference(); function inferPreference() { let usesJsExtensions = false; - const specifiers = sourceFile.imports.length ? sourceFile.imports : - isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0]) : + const specifiers = sourceFile?.imports.length ? sourceFile.imports : + sourceFile && isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0]) : emptyArray; for (const specifier of specifiers) { if (pathIsRelative(specifier.text)) { if ( moduleResolutionIsNodeNext && resolutionMode === ModuleKind.CommonJS && - getModeForUsageLocation(sourceFile, specifier, compilerOptions) === ModuleKind.ESNext + getModeForUsageLocation(sourceFile!, specifier, compilerOptions) === ModuleKind.ESNext ) { // We're trying to decide a preference for a CommonJS module specifier, but looking at an ESM import. continue; diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 78df7b3a900..9dc8ebefe7d 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -3,6 +3,7 @@ import { AnyImportOrRequireStatement, AnyImportSyntax, arrayFrom, + BindingElement, CancellationToken, cast, changeAnyExtension, @@ -15,6 +16,7 @@ import { compareValues, Comparison, CompilerOptions, + createFutureSourceFile, createModuleSpecifierResolutionHost, createMultiMap, createPackageJsonImportFilter, @@ -27,12 +29,15 @@ import { ExportKind, ExportMapInfoKey, factory, + findAncestor, first, firstDefined, flatMap, flatMapIterator, forEachExternalModuleToImportFrom, formatting, + FutureSourceFile, + FutureSymbolExportInfo, getAllowSyntheticDefaultImports, getBaseFileName, getDefaultExportInfoWorker, @@ -45,32 +50,34 @@ import { getMeaningFromDeclaration, getMeaningFromLocation, getNameForExportedSymbol, - getNodeId, getOutputExtension, getQuoteFromPreference, getQuotePreference, getSourceFileOfNode, getSymbolId, + getSynthesizedDeepClone, getTokenAtPosition, getTokenPosOfNode, getTypeKeywordOfTypeOnlyImport, getUniqueSymbolId, + hasJSFileExtension, hostGetCanonicalFileName, Identifier, ImportClause, ImportEqualsDeclaration, importFromModuleSpecifier, ImportKind, + ImportSpecifier, insertImports, InternalSymbolName, - isExternalModule, isExternalModuleReference, + isFullSourceFile, isIdentifier, isIdentifierPart, isIdentifierStart, isImportableFile, + isImportDeclaration, isImportEqualsDeclaration, - isInJSFile, isIntrinsicJsxName, isJSDocImportTag, isJsxClosingElement, @@ -79,6 +86,7 @@ import { isJSXTagName, isNamedImports, isNamespaceImport, + isRequireVariableStatement, isSourceFileJS, isStringANonContextualKeyword, isStringLiteral, @@ -101,6 +109,7 @@ import { MultiMap, Mutable, NamedImports, + NamespaceImport, Node, NodeFlags, nodeIsMissing, @@ -114,6 +123,7 @@ import { QuotePreference, removeFileExtension, removeSuffix, + RequireOrImportCall, RequireVariableStatement, sameMap, ScriptTarget, @@ -140,6 +150,7 @@ import { TypeChecker, TypeOnlyAliasDeclaration, UserPreferences, + VariableDeclarationInitializedTo, } from "../_namespaces/ts"; import { createCodeFixAction, @@ -200,6 +211,14 @@ registerCodeFix({ }, }); +/** + * The node kinds that may be the declaration of an alias symbol imported/required from an external module. + * `ImportClause` is the declaration for a syntactic default import. `VariableDeclaration` is the declaration + * for a non-destructured `require` call. + * @internal + */ +export type ImportOrRequireAliasDeclaration = ImportEqualsDeclaration | ImportClause | ImportSpecifier | NamespaceImport | VariableDeclarationInitializedTo | BindingElement; + /** * Computes multiple import additions to a file and writes them to a ChangeTracker. * @@ -208,12 +227,16 @@ registerCodeFix({ export interface ImportAdder { hasFixes(): boolean; addImportFromDiagnostic: (diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) => void; - addImportFromExportedSymbol: (exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) => void; + addImportFromExportedSymbol: (exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean, referenceImport?: ImportOrRequireAliasDeclaration) => void; + addImportForNonExistentExport: (exportName: string, exportingFileName: string, exportKind: ExportKind, exportedMeanings: SymbolFlags, isImportUsageValidAsTypeOnly: boolean) => void; + addImportForUnresolvedIdentifier: (context: CodeFixContextBase, symbolToken: Identifier, useAutoImportProvider: boolean) => void; + addVerbatimImport: (declaration: AnyImportOrRequireStatement | ImportOrRequireAliasDeclaration) => void; + removeExistingImport: (declaration: ImportOrRequireAliasDeclaration) => void; writeFixes: (changeTracker: textChanges.ChangeTracker, oldFileQuotePreference?: QuotePreference) => void; } /** @internal */ -export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { +export function createImportAdder(sourceFile: SourceFile | FutureSourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host, cancellationToken); } @@ -223,18 +246,29 @@ interface AddToExistingState { readonly namedImports: Map; } -function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { +function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { const compilerOptions = program.getCompilerOptions(); // Namespace fixes don't conflict, so just build a list. const addToNamespace: FixUseNamespaceImport[] = []; const importType: FixAddJsdocTypeImport[] = []; - /** Keys are import clause node IDs. */ - const addToExisting = new Map(); + const addToExisting = new Map(); + const removeExisting = new Set(); + const verbatimImports = new Set(); type NewImportsKey = `${0 | 1}|${string}`; /** Use `getNewImportEntry` for access */ const newImports = new Map>(); - return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes, hasFixes }; + return { addImportFromDiagnostic, addImportFromExportedSymbol, writeFixes, hasFixes, addImportForUnresolvedIdentifier, addImportForNonExistentExport, removeExistingImport, addVerbatimImport }; + + function addVerbatimImport(declaration: AnyImportOrRequireStatement | ImportOrRequireAliasDeclaration) { + verbatimImports.add(declaration); + } + + function addImportForUnresolvedIdentifier(context: CodeFixContextBase, symbolToken: Identifier, useAutoImportProvider: boolean) { + const info = getFixInfosWithoutDiagnostic(context, symbolToken, useAutoImportProvider); + if (!info || !info.length) return; + addImport(first(info)); + } function addImportFromDiagnostic(diagnostic: DiagnosticWithLocation, context: CodeFixContextBase) { const info = getFixInfos(context, diagnostic.code, diagnostic.start, useAutoImportProvider); @@ -242,19 +276,89 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu addImport(first(info)); } - function addImportFromExportedSymbol(exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean) { + function addImportFromExportedSymbol(exportedSymbol: Symbol, isValidTypeOnlyUseSite?: boolean, referenceImport?: ImportOrRequireAliasDeclaration) { const moduleSymbol = Debug.checkDefined(exportedSymbol.parent); const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions)); const checker = program.getTypeChecker(); const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); const exportInfo = getAllExportInfoForSymbol(sourceFile, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); const useRequire = shouldUseRequire(sourceFile, program); - const fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); + let fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); if (fix) { - addImport({ fix, symbolName, errorIdentifierText: undefined }); + const localName = tryCast(referenceImport?.name, isIdentifier)?.text ?? symbolName; + if ( + referenceImport + && isTypeOnlyImportDeclaration(referenceImport) + && (fix.kind === ImportFixKind.AddNew || fix.kind === ImportFixKind.AddToExisting) + && fix.addAsTypeOnly === AddAsTypeOnly.Allowed + ) { + // Copy the type-only status from the reference import + fix = { ...fix, addAsTypeOnly: AddAsTypeOnly.Required }; + } + addImport({ fix, symbolName: localName ?? symbolName, errorIdentifierText: undefined }); } } + function addImportForNonExistentExport(exportName: string, exportingFileName: string, exportKind: ExportKind, exportedMeanings: SymbolFlags, isImportUsageValidAsTypeOnly: boolean) { + const exportingSourceFile = program.getSourceFile(exportingFileName); + const useRequire = shouldUseRequire(sourceFile, program); + if (exportingSourceFile && exportingSourceFile.symbol) { + const { fixes } = getImportFixes( + [{ + exportKind, + isFromPackageJson: false, + moduleFileName: exportingFileName, + moduleSymbol: exportingSourceFile.symbol, + targetFlags: exportedMeanings, + }], + /*usagePosition*/ undefined, + isImportUsageValidAsTypeOnly, + useRequire, + program, + sourceFile, + host, + preferences, + ); + if (fixes.length) { + addImport({ fix: fixes[0], symbolName: exportName, errorIdentifierText: exportName }); + } + } + else { + // File does not exist yet or has no exports, so all imports added will be "new" + const futureExportingSourceFile = createFutureSourceFile(exportingFileName, ModuleKind.ESNext, program, host); + const moduleSpecifier = moduleSpecifiers.getLocalModuleSpecifierBetweenFileNames( + sourceFile, + exportingFileName, + compilerOptions, + createModuleSpecifierResolutionHost(program, host), + ); + const importKind = getImportKind(futureExportingSourceFile, exportKind, compilerOptions); + const addAsTypeOnly = getAddAsTypeOnly( + isImportUsageValidAsTypeOnly, + /*isForNewImportDeclaration*/ true, + /*symbol*/ undefined, + exportedMeanings, + program.getTypeChecker(), + compilerOptions, + ); + const fix: FixAddNewImport = { + kind: ImportFixKind.AddNew, + moduleSpecifier, + importKind, + addAsTypeOnly, + useRequire, + }; + addImport({ fix, symbolName: exportName, errorIdentifierText: exportName }); + } + } + + function removeExistingImport(declaration: ImportOrRequireAliasDeclaration) { + if (declaration.kind === SyntaxKind.ImportClause) { + Debug.assertIsDefined(declaration.name, "ImportClause should have a name if it's being removed"); + } + removeExisting.add(declaration); + } + function addImport(info: FixInfo) { const { fix, symbolName } = info; switch (fix.kind) { @@ -266,10 +370,9 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu break; case ImportFixKind.AddToExisting: { const { importClauseOrBindingPattern, importKind, addAsTypeOnly } = fix; - const key = String(getNodeId(importClauseOrBindingPattern)); - let entry = addToExisting.get(key); + let entry = addToExisting.get(importClauseOrBindingPattern); if (!entry) { - addToExisting.set(key, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: new Map() }); + addToExisting.set(importClauseOrBindingPattern, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: new Map() }); } if (importKind === ImportKind.Named) { const prevValue = entry?.namedImports.get(symbolName); @@ -362,7 +465,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu function writeFixes(changeTracker: textChanges.ChangeTracker, oldFileQuotePreference?: QuotePreference) { let quotePreference: QuotePreference; - if (sourceFile.imports.length === 0 && oldFileQuotePreference !== undefined) { + if (isFullSourceFile(sourceFile) && sourceFile.imports.length === 0 && oldFileQuotePreference !== undefined) { // If the target file has no imports, we must use the same quote preference as the file we are importing from. quotePreference = oldFileQuotePreference; } @@ -370,18 +473,102 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu quotePreference = getQuotePreference(sourceFile, preferences); } for (const fix of addToNamespace) { - addNamespaceQualifier(changeTracker, sourceFile, fix); + // Any modifications to existing syntax imply SourceFile already exists + addNamespaceQualifier(changeTracker, sourceFile as SourceFile, fix); } for (const fix of importType) { - addImportType(changeTracker, sourceFile, fix, quotePreference); + // Any modifications to existing syntax imply SourceFile already exists + addImportType(changeTracker, sourceFile as SourceFile, fix, quotePreference); + } + let importSpecifiersToRemoveWhileAdding: Set | undefined; + if (removeExisting.size) { + Debug.assert(isFullSourceFile(sourceFile), "Cannot remove imports from a future source file"); + const importDeclarationsWithRemovals = new Set(mapDefined([...removeExisting], d => findAncestor(d, isImportDeclaration)!)); + const variableDeclarationsWithRemovals = new Set(mapDefined([...removeExisting], d => findAncestor(d, isVariableDeclarationInitializedToRequire)!)); + const emptyImportDeclarations = [...importDeclarationsWithRemovals].filter(d => + // nothing added to the import declaration + !addToExisting.has(d.importClause!) && + // no default, or default is being removed + (!d.importClause?.name || removeExisting.has(d.importClause)) && + // no namespace import, or namespace import is being removed + (!tryCast(d.importClause?.namedBindings, isNamespaceImport) || removeExisting.has(d.importClause!.namedBindings as NamespaceImport)) && + // no named imports, or all named imports are being removed + (!tryCast(d.importClause?.namedBindings, isNamedImports) || every((d.importClause!.namedBindings as NamedImports).elements, e => removeExisting.has(e))) + ); + const emptyVariableDeclarations = [...variableDeclarationsWithRemovals].filter(d => + // no binding elements being added to the variable declaration + (d.name.kind !== SyntaxKind.ObjectBindingPattern || !addToExisting.has(d.name)) && + // no binding elements, or all binding elements are being removed + (d.name.kind !== SyntaxKind.ObjectBindingPattern || every(d.name.elements, e => removeExisting.has(e))) + ); + const namedBindingsToDelete = [...importDeclarationsWithRemovals].filter(d => + // has named bindings + d.importClause?.namedBindings && + // is not being fully removed + emptyImportDeclarations.indexOf(d) === -1 && + // is not gaining named imports + !addToExisting.get(d.importClause)?.namedImports && + // all named imports are being removed + (d.importClause.namedBindings.kind === SyntaxKind.NamespaceImport || every(d.importClause.namedBindings.elements, e => removeExisting.has(e))) + ); + for (const declaration of [...emptyImportDeclarations, ...emptyVariableDeclarations]) { + changeTracker.delete(sourceFile, declaration); + } + for (const declaration of namedBindingsToDelete) { + changeTracker.replaceNode( + sourceFile, + declaration.importClause!, + factory.updateImportClause( + declaration.importClause!, + declaration.importClause!.isTypeOnly, + declaration.importClause!.name, + /*namedBindings*/ undefined, + ), + ); + } + for (const declaration of removeExisting) { + const importDeclaration = findAncestor(declaration, isImportDeclaration); + if ( + importDeclaration && + emptyImportDeclarations.indexOf(importDeclaration) === -1 && + namedBindingsToDelete.indexOf(importDeclaration) === -1 + ) { + if (declaration.kind === SyntaxKind.ImportClause) { + changeTracker.delete(sourceFile, declaration.name!); + } + else { + Debug.assert(declaration.kind === SyntaxKind.ImportSpecifier, "NamespaceImport should have been handled earlier"); + if (addToExisting.get(importDeclaration.importClause!)?.namedImports) { + // Handle combined inserts/deletes in `doAddExistingFix` + (importSpecifiersToRemoveWhileAdding ??= new Set()).add(declaration); + } + else { + changeTracker.delete(sourceFile, declaration); + } + } + } + else if (declaration.kind === SyntaxKind.BindingElement) { + if (addToExisting.get(declaration.parent as ObjectBindingPattern)?.namedImports) { + // Handle combined inserts/deletes in `doAddExistingFix` + (importSpecifiersToRemoveWhileAdding ??= new Set()).add(declaration); + } + else { + changeTracker.delete(sourceFile, declaration); + } + } + else if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) { + changeTracker.delete(sourceFile, declaration); + } + } } addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports }) => { doAddExistingFix( changeTracker, - sourceFile, + sourceFile as SourceFile, importClauseOrBindingPattern, defaultImport, arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })), + importSpecifiersToRemoveWhileAdding, preferences, ); }); @@ -401,13 +588,84 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu ); newDeclarations = combine(newDeclarations, declarations); }); + newDeclarations = combine(newDeclarations, getCombinedVerbatimImports()); if (newDeclarations) { insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true, preferences); } } + function getCombinedVerbatimImports(): AnyImportOrRequireStatement[] | undefined { + if (!verbatimImports.size) return undefined; + const importDeclarations = new Set(mapDefined([...verbatimImports], d => findAncestor(d, isImportDeclaration))); + const requireStatements = new Set(mapDefined([...verbatimImports], d => findAncestor(d, isRequireVariableStatement))); + return [ + ...mapDefined([...verbatimImports], d => + d.kind === SyntaxKind.ImportEqualsDeclaration + ? getSynthesizedDeepClone(d, /*includeTrivia*/ true) + : undefined), + ...[...importDeclarations].map(d => { + if (verbatimImports.has(d)) { + return getSynthesizedDeepClone(d, /*includeTrivia*/ true); + } + return getSynthesizedDeepClone( + factory.updateImportDeclaration( + d, + d.modifiers, + d.importClause && factory.updateImportClause( + d.importClause, + d.importClause.isTypeOnly, + verbatimImports.has(d.importClause) ? d.importClause.name : undefined, + verbatimImports.has(d.importClause.namedBindings as NamespaceImport) + ? d.importClause.namedBindings as NamespaceImport : + tryCast(d.importClause.namedBindings, isNamedImports)?.elements.some(e => verbatimImports.has(e)) + ? factory.updateNamedImports( + d.importClause.namedBindings as NamedImports, + (d.importClause.namedBindings as NamedImports).elements.filter(e => verbatimImports.has(e)), + ) + : undefined, + ), + d.moduleSpecifier, + d.attributes, + ), + /*includeTrivia*/ true, + ); + }), + ...[...requireStatements].map(s => { + if (verbatimImports.has(s)) { + return getSynthesizedDeepClone(s, /*includeTrivia*/ true); + } + return getSynthesizedDeepClone( + factory.updateVariableStatement( + s, + s.modifiers, + factory.updateVariableDeclarationList( + s.declarationList, + mapDefined(s.declarationList.declarations, d => { + if (verbatimImports.has(d)) { + return d; + } + return factory.updateVariableDeclaration( + d, + d.name.kind === SyntaxKind.ObjectBindingPattern + ? factory.updateObjectBindingPattern( + d.name, + d.name.elements.filter(e => verbatimImports.has(e)), + ) : d.name, + d.exclamationToken, + d.type, + d.initializer, + ); + }), + ), + ), + /*includeTrivia*/ true, + ) as RequireVariableStatement; + }), + ]; + } + function hasFixes() { - return addToNamespace.length > 0 || importType.length > 0 || addToExisting.size > 0 || newImports.size > 0; + return addToNamespace.length > 0 || importType.length > 0 || addToExisting.size > 0 || newImports.size > 0 || verbatimImports.size > 0 || removeExisting.size > 0; } } @@ -422,7 +680,7 @@ export interface ImportSpecifierResolver { position: number, isValidTypeOnlyUseSite: boolean, fromCacheOnly?: boolean, - ): { exportInfo?: SymbolExportInfo; moduleSpecifier: string; computedWithoutCacheCount: number; } | undefined; + ): { exportInfo?: SymbolExportInfo | FutureSymbolExportInfo; moduleSpecifier: string; computedWithoutCacheCount: number; } | undefined; } /** @internal */ @@ -436,7 +694,7 @@ export function createImportSpecifierResolver(importingFile: SourceFile, program position: number, isValidTypeOnlyUseSite: boolean, fromCacheOnly?: boolean, - ): { exportInfo?: SymbolExportInfo; moduleSpecifier: string; computedWithoutCacheCount: number; } | undefined { + ): { exportInfo?: SymbolExportInfo | FutureSymbolExportInfo; moduleSpecifier: string; computedWithoutCacheCount: number; } | undefined { const { fixes, computedWithoutCacheCount } = getImportFixes( exportInfo, position, @@ -477,7 +735,7 @@ type ImportFixWithModuleSpecifier = FixUseNamespaceImport | FixAddJsdocTypeImpor // Properties are be undefined if fix is derived from an existing import interface ImportFixBase { readonly isReExport?: boolean; - readonly exportInfo?: SymbolExportInfo; + readonly exportInfo?: SymbolExportInfo | FutureSymbolExportInfo; readonly moduleSpecifier: string; } interface Qualification { @@ -491,7 +749,7 @@ interface FixAddJsdocTypeImport extends ImportFixBase { readonly kind: ImportFixKind.JsdocTypeImport; readonly usagePosition: number; readonly isReExport: boolean; - readonly exportInfo: SymbolExportInfo; + readonly exportInfo: SymbolExportInfo | FutureSymbolExportInfo; } interface FixAddToExistingImport extends ImportFixBase { readonly kind: ImportFixKind.AddToExisting; @@ -516,7 +774,7 @@ interface FixAddToExistingImportInfo { readonly declaration: AnyImportOrRequire; readonly importKind: ImportKind; readonly targetFlags: SymbolFlags; - readonly symbol: Symbol; + readonly symbol?: Symbol; } /** @internal */ @@ -584,7 +842,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo )); } -function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { +function getImportFixForSymbol(sourceFile: SourceFile | FutureSourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { const packageJsonImportFilter = createPackageJsonImportFilter(sourceFile, preferences, host); return getBestFix(getImportFixes(exportInfos, position, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences).fixes, sourceFile, program, packageJsonImportFilter, host); } @@ -593,7 +851,7 @@ function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAc return { description, changes, commands }; } -function getAllExportInfoForSymbol(importingFile: SourceFile, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { +function getAllExportInfoForSymbol(importingFile: SourceFile | FutureSourceFile, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { const getChecker = createGetChecker(program, host); return getExportInfoMap(importingFile, host, program, preferences, cancellationToken) .search(importingFile.path, preferCapitalized, name => name === symbolName, info => { @@ -624,20 +882,24 @@ function getSingleExportInfoForSymbol(symbol: Symbol, symbolName: string, module } } +function isFutureSymbolExportInfoArray(info: readonly SymbolExportInfo[] | readonly FutureSymbolExportInfo[]): info is readonly FutureSymbolExportInfo[] { + return info[0].symbol === undefined; +} + function getImportFixes( - exportInfos: readonly SymbolExportInfo[], + exportInfos: readonly SymbolExportInfo[] | readonly FutureSymbolExportInfo[], usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, host: LanguageServiceHost, preferences: UserPreferences, - importMap = createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), + importMap = isFullSourceFile(sourceFile) ? createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()) : undefined, fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number; fixes: readonly ImportFixWithModuleSpecifier[]; } { const checker = program.getTypeChecker(); - const existingImports = flatMap(exportInfos, importMap.getImportsForExportInfo); + const existingImports = importMap && !isFutureSymbolExportInfoArray(exportInfos) ? flatMap(exportInfos, importMap.getImportsForExportInfo) : emptyArray; const useNamespace = usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); if (addToExisting) { @@ -706,7 +968,7 @@ function getNamespaceLikeImportText(declaration: AnyImportOrRequire) { function getAddAsTypeOnly( isValidTypeOnlyUseSite: boolean, isForNewImportDeclaration: boolean, - symbol: Symbol, + symbol: Symbol | undefined, targetFlags: SymbolFlags, checker: TypeChecker, compilerOptions: CompilerOptions, @@ -716,6 +978,7 @@ function getAddAsTypeOnly( return AddAsTypeOnly.NotAllowed; } if ( + symbol && compilerOptions.verbatimModuleSyntax && (!(targetFlags & SymbolFlags.Value) || !!checker.getTypeOnlyAliasDeclaration(symbol)) ) { @@ -834,9 +1097,9 @@ function createExistingImportMap(checker: TypeChecker, importingFile: SourceFile }; } -function shouldUseRequire(sourceFile: SourceFile, program: Program): boolean { +function shouldUseRequire(sourceFile: SourceFile | FutureSourceFile, program: Program): boolean { // 1. TypeScript files don't use require variable declarations - if (!isSourceFileJS(sourceFile)) { + if (!hasJSFileExtension(sourceFile.fileName)) { return false; } @@ -872,29 +1135,29 @@ function createGetChecker(program: Program, host: LanguageServiceHost) { function getNewImportFixes( program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, - exportInfo: readonly SymbolExportInfo[], + exportInfo: readonly (SymbolExportInfo | FutureSymbolExportInfo)[], host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number; fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[]; } { - const isJs = isSourceFileJS(sourceFile); + const isJs = hasJSFileExtension(sourceFile.fileName); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); const getChecker = createGetChecker(program, host); const moduleResolution = getEmitModuleResolutionKind(compilerOptions); const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(moduleResolution); const getModuleSpecifiers = fromCacheOnly - ? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) - : (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences, /*options*/ undefined, /*forAutoImport*/ true); + ? (exportInfo: SymbolExportInfo | FutureSymbolExportInfo) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(exportInfo.moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) + : (exportInfo: SymbolExportInfo | FutureSymbolExportInfo, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(exportInfo.moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences, /*options*/ undefined, /*forAutoImport*/ true); let computedWithoutCacheCount = 0; const fixes = flatMap(exportInfo, (exportInfo, i) => { const checker = getChecker(exportInfo.isFromPackageJson); - const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol, checker); + const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo, checker); const importedSymbolHasValueMeaning = !!(exportInfo.targetFlags & SymbolFlags.Value); const addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ true, exportInfo.symbol, exportInfo.targetFlags, checker, compilerOptions); computedWithoutCacheCount += computedWithoutCache ? 1 : 0; @@ -944,10 +1207,10 @@ function getNewImportFixes( } function getFixesForAddImport( - exportInfos: readonly SymbolExportInfo[], + exportInfos: readonly SymbolExportInfo[] | readonly FutureSymbolExportInfo[], existingImports: readonly FixAddToExistingImportInfo[], program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -1011,7 +1274,13 @@ function sortFixInfo(fixes: readonly (FixInfo & { fix: ImportFixWithModuleSpecif compareModuleSpecifiers(a.fix, b.fix, sourceFile, program, packageJsonImportFilter.allowsImportingSpecifier, _toPath)); } -function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { +function getFixInfosWithoutDiagnostic(context: CodeFixContextBase, symbolToken: Identifier, useAutoImportProvider: boolean): readonly FixInfo[] | undefined { + const info = getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider); + const packageJsonImportFilter = createPackageJsonImportFilter(context.sourceFile, context.preferences, context.host); + return info && sortFixInfo(info, context.sourceFile, context.program, packageJsonImportFilter, context.host); +} + +function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile | FutureSourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { if (!some(fixes)) return; // These will always be placed first if available, and are better than other kinds if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) { @@ -1035,7 +1304,7 @@ function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: function compareModuleSpecifiers( a: ImportFixWithModuleSpecifier, b: ImportFixWithModuleSpecifier, - importingFile: SourceFile, + importingFile: SourceFile | FutureSourceFile, program: Program, allowsImportingSpecifier: (specifier: string) => boolean, toPath: (fileName: string) => Path, @@ -1044,8 +1313,8 @@ function compareModuleSpecifiers( return compareBooleans(allowsImportingSpecifier(b.moduleSpecifier), allowsImportingSpecifier(a.moduleSpecifier)) || compareNodeCoreModuleSpecifiers(a.moduleSpecifier, b.moduleSpecifier, importingFile, program) || compareBooleans( - isFixPossiblyReExportingImportingFile(a, importingFile, program.getCompilerOptions(), toPath), - isFixPossiblyReExportingImportingFile(b, importingFile, program.getCompilerOptions(), toPath), + isFixPossiblyReExportingImportingFile(a, importingFile.path, toPath), + isFixPossiblyReExportingImportingFile(b, importingFile.path, toPath), ) || compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier); } @@ -1056,14 +1325,14 @@ function compareModuleSpecifiers( // E.g., do not `import { Foo } from ".."` when you could `import { Foo } from "../Foo"`. // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index". -function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { +function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFilePath: Path, toPath: (fileName: string) => Path): boolean { if ( fix.isReExport && fix.exportInfo?.moduleFileName && isIndexFileName(fix.exportInfo.moduleFileName) ) { const reExportDir = toPath(getDirectoryPath(fix.exportInfo.moduleFileName)); - return startsWith(importingFile.path, reExportDir); + return startsWith(importingFilePath, reExportDir); } return false; } @@ -1072,7 +1341,7 @@ function isIndexFileName(fileName: string) { return getBaseFileName(fileName, [".js", ".jsx", ".d.ts", ".ts", ".tsx"], /*ignoreCase*/ true) === "index"; } -function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile, program: Program): Comparison { +function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile | FutureSourceFile, program: Program): Comparison { if (startsWith(a, "node:") && !startsWith(b, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.LessThan : Comparison.GreaterThan; if (startsWith(b, "node:") && !startsWith(a, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.GreaterThan : Comparison.LessThan; return Comparison.EqualTo; @@ -1117,7 +1386,7 @@ function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { * * @internal */ -export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { +export function getImportKind(importingFile: SourceFile | FutureSourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { if (compilerOptions.verbatimModuleSyntax && (getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS || importingFile.impliedNodeFormat === ModuleKind.CommonJS)) { // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible return ImportKind.CommonJS; @@ -1136,7 +1405,7 @@ export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, } } -function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getUmdImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { // Import a synthetic `default` if enabled. if (getAllowSyntheticDefaultImports(compilerOptions)) { return ImportKind.Default; @@ -1148,8 +1417,8 @@ function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOp case ModuleKind.AMD: case ModuleKind.CommonJS: case ModuleKind.UMD: - if (isInJSFile(importingFile)) { - return isExternalModule(importingFile) || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; + if (hasJSFileExtension(importingFile.fileName)) { + return importingFile.externalModuleIndicator || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; } return ImportKind.CommonJS; case ModuleKind.System: @@ -1265,9 +1534,9 @@ function getExportInfos( return originalSymbolToExportInfos; } -function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getExportEqualsImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { const allowSyntheticDefaults = getAllowSyntheticDefaultImports(compilerOptions); - const isJS = isInJSFile(importingFile); + const isJS = hasJSFileExtension(importingFile.fileName); // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. if (!isJS && getEmitModuleKind(compilerOptions) >= ModuleKind.ES2015) { @@ -1276,14 +1545,14 @@ function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: C // 2. 'import =' will not work in JavaScript, so the decision is between a default import, // a namespace import, and const/require. if (isJS) { - return isExternalModule(importingFile) || forceImportKeyword + return importingFile.externalModuleIndicator || forceImportKeyword ? allowSyntheticDefaults ? ImportKind.Default : ImportKind.Namespace : ImportKind.CommonJS; } // 3. At this point the most correct choice is probably 'import =', but people // really hate that, so look to see if the importing file has any precedent // on how to handle it. - for (const statement of importingFile.statements) { + for (const statement of importingFile.statements ?? emptyArray) { // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { return ImportKind.CommonJS; @@ -1334,6 +1603,7 @@ function codeActionForFixWorker( importClauseOrBindingPattern, importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined, importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : emptyArray, + /*removeExistingImportSpecifiers*/ undefined, preferences, ); const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); @@ -1474,9 +1744,25 @@ function doAddExistingFix( clause: ImportClause | ObjectBindingPattern, defaultImport: Import | undefined, namedImports: readonly Import[], + removeExistingImportSpecifiers: Set | undefined, preferences: UserPreferences, ): void { if (clause.kind === SyntaxKind.ObjectBindingPattern) { + if (removeExistingImportSpecifiers && clause.elements.some(e => removeExistingImportSpecifiers.has(e))) { + // If we're both adding and removing elements, just replace and reprint the whole + // node. The change tracker doesn't understand all the operations and can insert or + // leave behind stray commas. + changes.replaceNode( + sourceFile, + clause, + factory.createObjectBindingPattern([ + ...clause.elements.filter(e => !removeExistingImportSpecifiers.has(e)), + ...defaultImport ? [factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ "default", defaultImport.name)] : emptyArray, + ...namedImports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i.name)), + ]), + ); + return; + } if (defaultImport) { addElementToBindingPattern(clause, defaultImport.name, "default"); } @@ -1508,6 +1794,19 @@ function doAddExistingFix( specifierComparer, ); + if (removeExistingImportSpecifiers) { + // If we're both adding and removing specifiers, just replace and reprint the whole + // node. The change tracker doesn't understand all the operations and can insert or + // leave behind stray commas. + changes.replaceNode( + sourceFile, + clause.namedBindings!, + factory.updateNamedImports( + clause.namedBindings as NamedImports, + stableSort([...existingSpecifiers!.filter(s => !removeExistingImportSpecifiers.has(s)), ...newSpecifiers], specifierComparer), + ), + ); + } // The sorting preference computed earlier may or may not have validated that these particular // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return // nonsense. So if there are existing specifiers, even if we know the sorting preference, we @@ -1515,7 +1814,7 @@ function doAddExistingFix( // to do a sorted insertion. // changed to check if existing specifiers are sorted - if (existingSpecifiers?.length && isSorted !== false) { + else if (existingSpecifiers?.length && isSorted !== false) { // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports const transformedExistingSpecifiers = (promoteFromTypeOnly && existingSpecifiers) ? factory.updateNamedImports( clause.namedBindings as NamedImports, diff --git a/src/services/completions.ts b/src/services/completions.ts index 2a3e3126dd1..4d4d3c313ae 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -73,6 +73,7 @@ import { forEach, formatting, FunctionLikeDeclaration, + FutureSymbolExportInfo, getAllSuperTypeNodes, getAncestor, getCombinedLocalAndExportSymbolFlags, @@ -609,7 +610,7 @@ interface ModuleSpecifierResolutionContext { } type ModuleSpecifierResolutionResult = "skipped" | "failed" | { - exportInfo?: SymbolExportInfo; + exportInfo?: SymbolExportInfo | FutureSymbolExportInfo; moduleSpecifier: string; }; @@ -4079,20 +4080,20 @@ function getCompletionData( // it should be identical regardless of which one is used. During the subsequent // `CompletionEntryDetails` request, we'll get all the ExportInfos again and pick // the best one based on the module specifier it produces. - let exportInfo = info[0], moduleSpecifier; + let exportInfo: SymbolExportInfo | FutureSymbolExportInfo = info[0], moduleSpecifier; if (result !== "skipped") { ({ exportInfo = info[0], moduleSpecifier } = result); } const isDefaultExport = exportInfo.exportKind === ExportKind.Default; - const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; + const symbol = isDefaultExport && getLocalSymbolForExportDefault(Debug.checkDefined(exportInfo.symbol)) || Debug.checkDefined(exportInfo.symbol); pushAutoImportSymbol(symbol, { kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, exportMapKey, - exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, + exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : Debug.checkDefined(exportInfo.symbol).name, fileName: exportInfo.moduleFileName, isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 322631ad92c..f7c6521448d 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -13,6 +13,7 @@ import { firstDefined, forEachAncestorDirectory, forEachEntry, + FutureSourceFile, getBaseFileName, GetCanonicalFileName, getDirectoryPath, @@ -90,6 +91,12 @@ export interface SymbolExportInfo { isFromPackageJson: boolean; } +/** + * @internal + * ExportInfo for an export that does not exist yet, so does not have a symbol. + */ +export type FutureSymbolExportInfo = Omit & { readonly symbol?: undefined; }; + interface CachedSymbolExportInfo { // Used to rehydrate `symbol` and `moduleSymbol` when transient id: number; @@ -477,7 +484,7 @@ function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly So } /** @internal */ -export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { +export function getExportInfoMap(importingFile: SourceFile | FutureSourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { const start = timestamp(); // Pulling the AutoImportProvider project will trigger its updateGraph if pending, // which will invalidate the export map cache if things change, so pull it before diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index f15aff7173a..5887fba6ae2 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -655,7 +655,7 @@ export class ChangeTracker { this.insertNodesAt(sourceFile, pos, insert, options); } - private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { + public insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { if (!this.newFileChanges) { this.newFileChanges = createMultiMap(); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ca9ebafe08b..d8d05869a0b 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -90,6 +90,7 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, + FutureSourceFile, getAssignmentDeclarationKind, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, @@ -97,6 +98,7 @@ import { getEmitScriptTarget, getExternalModuleImportEqualsDeclarationExpression, getImpliedNodeFormatForFile, + getImpliedNodeFormatForFileWorker, getIndentString, getJSDocEnumTag, getLastChild, @@ -106,6 +108,7 @@ import { getModuleInstanceState, getNameOfDeclaration, getNodeId, + getOriginalNode, getPackageNameFromTypesPackageName, getPathComponents, getRootDeclaration, @@ -168,6 +171,7 @@ import { isFileLevelUniqueName, isForInStatement, isForOfStatement, + isFullSourceFile, isFunctionBlock, isFunctionDeclaration, isFunctionExpression, @@ -281,6 +285,7 @@ import { ModuleDeclaration, ModuleInstanceState, ModuleKind, + ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierResolutionHost, moduleSpecifiers, @@ -2533,13 +2538,13 @@ export function quotePreferenceFromString(str: StringLiteral, sourceFile: Source } /** @internal */ -export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { +export function getQuotePreference(sourceFile: SourceFile | FutureSourceFile, preferences: UserPreferences): QuotePreference { if (preferences.quotePreference && preferences.quotePreference !== "auto") { return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; } else { // ignore synthetic import added when importHelpers: true - const firstModuleSpecifier = sourceFile.imports && + const firstModuleSpecifier = isFullSourceFile(sourceFile) && sourceFile.imports && find(sourceFile.imports, n => isStringLiteral(n) && !nodeIsSynthesized(n.parent)) as StringLiteral; return firstModuleSpecifier ? quotePreferenceFromString(firstModuleSpecifier, sourceFile) : QuotePreference.Double; } @@ -2627,16 +2632,28 @@ export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | und } /** @internal */ -export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { +export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile | FutureSourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); const { comparer, isSorted } = OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; - if (!existingImportStatements.length) { - changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); + if (!existingImportStatements?.length) { + if (isFullSourceFile(sourceFile)) { + changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); + } + else { + for (const newImport of sortedNewImports) { + // Insert one at a time to send correct original source file for accurate text reuse + // when some imports are cloned from existing ones in other files. + changes.insertStatementsInNewFile(sourceFile.fileName, [newImport], getOriginalNode(newImport)?.getSourceFile()); + } + } + return; } - else if (existingImportStatements && isSorted) { + + Debug.assert(isFullSourceFile(sourceFile)); + if (existingImportStatements && isSorted) { for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); if (insertionIndex === 0) { @@ -3761,7 +3778,7 @@ export interface PackageJsonImportFilter { } /** @internal */ -export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { +export function createPackageJsonImportFilter(fromFile: SourceFile | FutureSourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { const packageJsons = ( (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) ).filter(p => p.parseable); @@ -3860,7 +3877,7 @@ export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: // from Node core modules or not. We can start by seeing if the user is actually using // any node core modules, as opposed to simply having @types/node accidentally as a // dependency of a dependency. - if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { + if (isFullSourceFile(fromFile) && isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { if (usesNodeCoreModules === undefined) { usesNodeCoreModules = consumesNodeCoreModules(fromFile); } @@ -4121,7 +4138,7 @@ export function isDeprecatedDeclaration(decl: Declaration) { } /** @internal */ -export function shouldUseUriStyleNodeCoreModules(file: SourceFile, program: Program): boolean { +export function shouldUseUriStyleNodeCoreModules(file: SourceFile | FutureSourceFile, program: Program): boolean { const decisionFromFile = firstDefined(file.imports, node => { if (JsTyping.nodeCoreModules.has(node.text)) { return startsWith(node.text, "node:"); @@ -4293,3 +4310,23 @@ export function isBlockLike(node: Node): node is BlockLike { return false; } } + +/** @internal */ +export function createFutureSourceFile(fileName: string, syntaxModuleIndicator: ModuleKind.ESNext | ModuleKind.CommonJS | undefined, program: Program, moduleResolutionHost: ModuleResolutionHost): FutureSourceFile { + const result = getImpliedNodeFormatForFileWorker(fileName, program.getPackageJsonInfoCache?.(), moduleResolutionHost, program.getCompilerOptions()); + let impliedNodeFormat, packageJsonScope; + if (typeof result === "object") { + impliedNodeFormat = result.impliedNodeFormat; + packageJsonScope = result.packageJsonScope; + } + return { + path: toPath(fileName, program.getCurrentDirectory(), program.getCanonicalFileName), + fileName, + externalModuleIndicator: syntaxModuleIndicator === ModuleKind.ESNext ? true : undefined, + commonJsModuleIndicator: syntaxModuleIndicator === ModuleKind.CommonJS ? true : undefined, + impliedNodeFormat, + packageJsonScope, + statements: emptyArray, + imports: emptyArray, + }; +}