Expand auto-import API to work on non-existent files and symbols (#58093)

This commit is contained in:
Andrew Branch 2024-04-09 11:24:15 -07:00 committed by GitHub
parent 7a4cbfa7ea
commit 5c55ce1ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 478 additions and 89 deletions

View File

@ -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<SourceFile, "fileName" | "impliedNodeFormat">,
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<SourceFile, "fileName" | "impliedNodeFormat">,
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;
}

View File

@ -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. */

View File

@ -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<SourceFile>)?.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;

View File

@ -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<RequireOrImportCall> | 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<string, AddAsTypeOnly>;
}
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<string, AddToExistingState>();
const addToExisting = new Map<ImportClause | ObjectBindingPattern, AddToExistingState>();
const removeExisting = new Set<ImportOrRequireAliasDeclaration>();
const verbatimImports = new Set<AnyImportOrRequireStatement | ImportOrRequireAliasDeclaration>();
type NewImportsKey = `${0 | 1}|${string}`;
/** Use `getNewImportEntry` for access */
const newImports = new Map<NewImportsKey, Mutable<ImportsCollection & { useRequire: boolean; }>>();
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<ImportSpecifier | BindingElement> | 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<ImportSpecifier | BindingElement> | 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,

View File

@ -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,

View File

@ -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<SymbolExportInfo, "symbol"> & { 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

View File

@ -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<string, NewFileInsertion>();
}

View File

@ -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,
};
}