diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 7b3ab92f60b..9088d6ee482 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -31,6 +31,7 @@ import { flatten, forEach, forEachAncestorDirectory, + FutureSourceFile, getBaseFileName, GetCanonicalFileName, getConditions, @@ -126,7 +127,7 @@ interface Preferences { function getPreferences( { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, oldImportSpecifier?: string, ): Preferences { const preferredEnding = getPreferredEnding(); @@ -212,7 +213,7 @@ export function getModuleSpecifier( /** @internal */ export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, @@ -243,14 +244,14 @@ function getModuleSpecifierWorker( /** @internal */ export function tryGetModuleSpecifiersFromCache( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFilePath: Path, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] | undefined { return tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFile, + importingSourceFilePath, host, userPreferences, options)[0]; @@ -258,7 +259,7 @@ export function tryGetModuleSpecifiersFromCache( function tryGetModuleSpecifiersFromCacheWorker( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFilePath: Path, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -269,7 +270,7 @@ function tryGetModuleSpecifiersFromCacheWorker( } const cache = host.getModuleSpecifierCache?.(); - const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); + const cached = cache?.get(importingSourceFilePath, moduleSourceFile.path, userPreferences, options); return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; } @@ -303,7 +304,7 @@ export function getModuleSpecifiersWithCacheInfo( moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -315,7 +316,7 @@ export function getModuleSpecifiersWithCacheInfo( // eslint-disable-next-line prefer-const let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFile, + importingSourceFile.path, host, userPreferences, options @@ -333,14 +334,14 @@ export function getModuleSpecifiersWithCacheInfo( function computeModuleSpecifiers( modulePaths: readonly ModulePath[], compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] { const info = getInfo(importingSourceFile.path, host); const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); - const existingSpecifier = forEach(modulePaths, modulePath => forEach( + const existingSpecifier = importingSourceFile.kind && forEach(modulePaths, modulePath => forEach( host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; @@ -877,7 +878,7 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam return processEnding(shortest, allowedEndings, compilerOptions); } -function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { +function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: 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 7e14ff22ded..edbb47928c3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4220,6 +4220,15 @@ export interface SourceFileLike { getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } +/** @internal */ +export interface FutureSourceFile { + readonly kind?: undefined; + readonly fileName: string; + readonly path: Path; + readonly impliedNodeFormat?: ResolutionMode; + readonly commonJsModuleIndicator?: boolean; + readonly externalModuleIndicator?: boolean; +} /** @internal */ export interface RedirectInfo { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index bf657d5a15d..65b5aa5be2e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -157,6 +157,7 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, + FutureSourceFile, GetAccessorDeclaration, getBaseFileName, GetCanonicalFileName, @@ -9160,7 +9161,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 | FutureSourceFile): ModuleSpecifierEnding { if (preference === "js" || resolutionMode === ModuleKind.ESNext) { // Extensions are explicitly requested or required. Now choose between .js and .ts. if (!shouldAllowImportingTsExtension(compilerOptions)) { @@ -9185,27 +9186,30 @@ 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.kind && usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } return inferPreference(); function inferPreference() { - let usesJsExtensions = false; - const specifiers = sourceFile.imports.length ? sourceFile.imports.map(i => i.text) : - isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0].text) : - emptyArray; - for (const specifier of specifiers) { - if (pathIsRelative(specifier)) { - if (hasTSFileExtension(specifier)) { - return ModuleSpecifierEnding.TsExtension; - } - if (hasJSFileExtension(specifier)) { - usesJsExtensions = true; + if (sourceFile.kind) { + let usesJsExtensions = false; + const specifiers = sourceFile.imports.length ? sourceFile.imports.map(i => i.text) : + isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0].text) : + emptyArray; + for (const specifier of specifiers) { + if (pathIsRelative(specifier)) { + if (hasTSFileExtension(specifier)) { + return ModuleSpecifierEnding.TsExtension; + } + if (hasJSFileExtension(specifier)) { + usesJsExtensions = true; + } } } + return usesJsExtensions ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } - return usesJsExtensions ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; + return ModuleSpecifierEnding.Minimal; } } diff --git a/src/services/codefixes/convertFunctionToEs6Class.ts b/src/services/codefixes/convertFunctionToEs6Class.ts index 5073d687a67..5c46bed9c57 100644 --- a/src/services/codefixes/convertFunctionToEs6Class.ts +++ b/src/services/codefixes/convertFunctionToEs6Class.ts @@ -21,7 +21,7 @@ import { FunctionExpression, getEmitScriptTarget, getNameOfDeclaration, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, idText, isAccessExpression, @@ -211,7 +211,7 @@ function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, po // f.x = expr if (isAccessExpression(memberDeclaration) && (isFunctionExpression(assignmentExpr) || isArrowFunction(assignmentExpr))) { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const name = tryGetPropertyName(memberDeclaration, compilerOptions, quotePreference); if (name) { createFunctionLikeExpressionMember(members, assignmentExpr, name); diff --git a/src/services/codefixes/convertToEsModule.ts b/src/services/codefixes/convertToEsModule.ts index b15a0521c1c..1f8b08d5e9b 100644 --- a/src/services/codefixes/convertToEsModule.ts +++ b/src/services/codefixes/convertToEsModule.ts @@ -27,7 +27,7 @@ import { FunctionExpression, getEmitScriptTarget, getModeForUsageLocation, - getQuotePreference, + getQuotePreferenceFromFile, getResolvedModule, getSynthesizedDeepClone, getSynthesizedDeepClones, @@ -87,10 +87,10 @@ registerCodeFix({ getCodeActions(context) { const { sourceFile, program, preferences } = context; const changes = textChanges.ChangeTracker.with(context, changes => { - const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreference(sourceFile, preferences)); + const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreferenceFromFile(sourceFile, preferences)); if (moduleExportsChangedToDefault) { for (const importingFile of program.getSourceFiles()) { - fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences)); + fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreferenceFromFile(importingFile, preferences)); } } }); diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 3d46bc1511d..464d58653a7 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -33,7 +33,7 @@ import { getNodeId, getObjectFlags, getOrUpdate, - getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getTokenAtPosition, hasAbstractModifier, @@ -597,7 +597,7 @@ function addEnumMemberDeclaration(changes: textChanges.ChangeTracker, checker: T } function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) { - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); const functionDeclaration = info.kind === InfoKind.Function ? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) @@ -614,7 +614,7 @@ function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: Cod function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const checker = context.program.getTypeChecker(); const jsxAttributesNode = info.parentDeclaration.attributes; const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute); @@ -634,7 +634,7 @@ function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixCo function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const target = getEmitScriptTarget(context.program.getCompilerOptions()); const checker = context.program.getTypeChecker(); const props = map(info.properties, prop => { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index bae3f2d2c0f..9a88b3b49fc 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -8,7 +8,7 @@ import { findAncestor, getEmitModuleKind, getNamespaceDeclarationNode, - getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getTokenAtPosition, ImportDeclaration, @@ -39,7 +39,7 @@ function getCodeFixesForImportDeclaration(context: CodeFixContext, node: ImportD const variations: CodeFixAction[] = []; // import Bluebird from "bluebird"; - variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreference(sourceFile, context.preferences)))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, context.preferences)))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts index 7a351260329..1e517583bf8 100644 --- a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts +++ b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts @@ -2,7 +2,7 @@ import { cast, Diagnostics, factory, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, isPropertyAccessChain, isPropertyAccessExpression, @@ -37,7 +37,7 @@ registerCodeFix({ }); function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: PropertyAccessExpression, preferences: UserPreferences): void { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const argumentsExpression = factory.createStringLiteral(node.name.text, quotePreference === QuotePreference.Single); changes.replaceNode( sourceFile, diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index e47e4c99777..00a878b049d 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -27,7 +27,7 @@ import { getModuleSpecifierResolverHost, getNameForExportedSymbol, getNameOfDeclaration, - getQuotePreference, + getQuotePreferenceFromFile, getSetAccessorValueParameter, getSynthesizedDeepClone, getTokenAtPosition, @@ -205,7 +205,7 @@ export function addNewNodeForMemberSymbol( const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); const optional = !!(symbol.flags & SymbolFlags.Optional); const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient; - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); switch (kind) { case SyntaxKind.PropertySignature: @@ -467,7 +467,7 @@ export function createSignatureDeclarationFromCallExpression( modifierFlags: ModifierFlags, contextNode: Node ): MethodDeclaration | FunctionDeclaration | MethodSignature { - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); const tracker = getNoopSymbolTrackerWithResolver(context); const checker = context.program.getTypeChecker(); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 6e7f9916748..feeeceed0b7 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -31,6 +31,7 @@ import { flatMapIterator, forEachExternalModuleToImportFrom, formatting, + FutureSourceFile, getAllowSyntheticDefaultImports, getBaseFileName, getDefaultExportInfoWorker, @@ -46,11 +47,13 @@ import { getNodeId, getQuoteFromPreference, getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getSymbolId, getTokenAtPosition, getTypeKeywordOfTypeOnlyImport, getUniqueSymbolId, + hasJSFileExtension, hostGetCanonicalFileName, Identifier, ImportClause, @@ -61,7 +64,6 @@ import { ImportsNotUsedAsValues, insertImports, InternalSymbolName, - isExternalModule, isExternalModuleReference, isIdentifier, isIdentifierPart, @@ -208,7 +210,7 @@ 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[] = []; @@ -232,7 +234,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu 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 exportInfo = getAllExportInfoForSymbol(sourceFile.path, 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); if (fix) { @@ -346,14 +348,17 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu } function writeFixes(changeTracker: textChanges.ChangeTracker) { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = sourceFile.kind ? getQuotePreferenceFromFile(sourceFile, preferences) : getQuotePreference(preferences); for (const fix of addToNamespace) { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addNamespaceQualifier(changeTracker, sourceFile, fix); } for (const fix of importType) { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addImportType(changeTracker, sourceFile, fix, quotePreference); } addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports }) => { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); doAddExistingFix( changeTracker, sourceFile, @@ -509,14 +514,14 @@ export function getImportCompletionAction( if (exportMapKey) { // The new way: `exportMapKey` should be in the `data` of each auto-import completion entry and // sent back when asking for details. - exportInfos = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); + exportInfos = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified exportMapKey"); } else { // The old way, kept alive for super old editors that don't give us `data` back. exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name)) ? [getSingleExportInfoForSymbol(targetSymbol, symbolName, moduleSymbol, program, host)] - : getAllExportInfoForSymbol(sourceFile, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); + : getAllExportInfoForSymbol(sourceFile.path, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified symbol / moduleSymbol"); } @@ -545,7 +550,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences)); } -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); } @@ -554,10 +559,10 @@ 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(importingFilePath: Path, 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 => { + return getExportInfoMap(importingFilePath, host, program, preferences, cancellationToken) + .search(importingFilePath, preferCapitalized, name => name === symbolName, info => { if (skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson)) === symbol && info.some(i => i.moduleSymbol === moduleSymbol || i.symbol.parent === moduleSymbol)) { return info; } @@ -591,16 +596,16 @@ function getImportFixes( isValidTypeOnlyUseSite: boolean, useRequire: boolean, program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, host: LanguageServiceHost, preferences: UserPreferences, - importMap = createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), + importMap = sourceFile.kind && createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly ImportFixWithModuleSpecifier[] } { const checker = program.getTypeChecker(); - const existingImports = flatMap(exportInfos, importMap.getImportsForExportInfo); - const useNamespace = usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); - const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); + const existingImports = importMap && flatMap(exportInfos, importMap.getImportsForExportInfo); + const useNamespace = existingImports && usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); + const addToExisting = existingImports && tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); if (addToExisting) { // Don't bother providing an action to add a new import if we can add to an existing one. return { @@ -787,9 +792,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 (sourceFile.kind && !isSourceFileJS(sourceFile) || !sourceFile.kind && !hasJSFileExtension(sourceFile.fileName)) { return false; } @@ -820,7 +825,7 @@ function createGetChecker(program: Program, host: LanguageServiceHost) { function getNewImportFixes( program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -829,14 +834,14 @@ function getNewImportFixes( preferences: UserPreferences, fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[] } { - const isJs = isSourceFileJS(sourceFile); + const isJs = sourceFile.kind ? isSourceFileJS(sourceFile) : 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) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile.path, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) : (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences); let computedWithoutCacheCount = 0; @@ -892,9 +897,9 @@ function getNewImportFixes( function getFixesForAddImport( exportInfos: readonly SymbolExportInfo[], - existingImports: readonly FixAddToExistingImportInfo[], + existingImports: readonly FixAddToExistingImportInfo[] | undefined, program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -958,7 +963,7 @@ 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 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) { @@ -982,7 +987,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, @@ -1003,7 +1008,7 @@ function compareModuleSpecifiers( // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index" (we don't even try to consider // this if we're in a resolution mode where you can't drop trailing "/index" from paths). -function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { +function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { if (fix.isReExport && fix.exportInfo?.moduleFileName && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node10 && @@ -1019,7 +1024,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; @@ -1064,7 +1069,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; @@ -1078,7 +1083,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; @@ -1090,8 +1095,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 (importingFile.kind ? isInJSFile(importingFile) : hasJSFileExtension(importingFile.fileName)) { + return importingFile.externalModuleIndicator || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; } return ImportKind.CommonJS; case ModuleKind.System: @@ -1206,9 +1211,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 = importingFile.kind ? isInJSFile(importingFile) : 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) { @@ -1217,17 +1222,19 @@ 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) { - // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration - if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { - return ImportKind.CommonJS; + if (importingFile.kind) { + for (const statement of importingFile.statements) { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { + return ImportKind.CommonJS; + } } } // 4. We have no precedent to go on, so just use a default import if @@ -1243,7 +1250,7 @@ function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: S return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports); } function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): DiagnosticOrDiagnosticAndArguments { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); switch (fix.kind) { case ImportFixKind.UseNamespace: addNamespaceQualifier(changes, sourceFile, fix); diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 75f72964530..5972b29ab1a 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -2,7 +2,7 @@ import { AnyImportSyntax, Diagnostics, Expression, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, Identifier, isExternalModuleReference, @@ -57,5 +57,5 @@ function getInfo(sourceFile: SourceFile, pos: number): Info | undefined { } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info, preferences: UserPreferences): void { - changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreference(sourceFile, preferences))); + changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, preferences))); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 43ae8c2497a..01fa6ea982a 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -96,7 +96,7 @@ import { getNewLineKind, getNewLineOrDefaultFromHost, getPropertyNameForPropertyNameNode, - getQuotePreference, + getQuotePreferenceFromFile, getReplacementSpanForContextToken, getRootDeclaration, getSourceFileOfModule, @@ -790,7 +790,7 @@ function continuePreviousIncompleteResponse( const touchNode = getTouchingPropertyName(file, position); const lowerCaseTokenText = location.text.toLowerCase(); - const exportMap = getExportInfoMap(file, host, program, preferences, cancellationToken); + const exportMap = getExportInfoMap(file.path, host, program, preferences, cancellationToken); const newEntries = resolvingModuleSpecifiers( "continuePreviousIncompleteResponse", host, @@ -1105,7 +1105,7 @@ function getJSDocParamAnnotation( const inferredType = checker.getTypeAtLocation(initializer.parent); if (!(inferredType.flags & (TypeFlags.Any | TypeFlags.Void))) { const sourceFile = initializer.getSourceFile(); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const builderFlags = (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); const typeNode = checker.typeToTypeNode(inferredType, findAncestor(initializer, isFunctionLike), builderFlags); if (typeNode) { @@ -1353,7 +1353,7 @@ function getExhaustiveCaseSnippets( const tracker = newCaseClauseTracker(checker, clauses); const target = getEmitScriptTarget(options); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); const elements: Expression[] = []; for (const type of switchType.types as LiteralType[]) { @@ -2048,7 +2048,7 @@ function createObjectLiteralMethod( const declaration = declarations[0]; const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const builderFlags = NodeBuilderFlags.OmitThisParameter | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); switch (declaration.kind) { @@ -3747,7 +3747,7 @@ function getCompletionData( ""; const moduleSpecifierCache = host.getModuleSpecifierCache?.(); - const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken); + const exportInfo = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken); const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host); resolvingModuleSpecifiers( diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 4651c1dac1c..d1029858b0a 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -451,7 +451,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(importingFilePath: Path, 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 @@ -463,7 +463,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), }); - if (cache.isUsableByFile(importingFile.path)) { + if (cache.isUsableByFile(importingFilePath)) { host.log?.("getExportInfoMap: cache hit"); return cache; } @@ -481,7 +481,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic // can cause it to happen: see 'completionsImport_mergedReExport.ts' if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { cache.add( - importingFile.path, + importingFilePath, defaultInfo.symbol, defaultInfo.exportKind === ExportKind.Default ? InternalSymbolName.Default : InternalSymbolName.ExportEquals, moduleSymbol, @@ -493,7 +493,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, key)) { cache.add( - importingFile.path, + importingFilePath, exported, key, moduleSymbol, diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 2fdff08a852..e420b6631a9 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -46,7 +46,7 @@ import { getLocaleSpecificMessage, getModifiers, getPropertySymbolFromBindingElement, - getQuotePreference, + getQuotePreferenceFromFile, getRangesWhere, getRefactorContextSpan, getRelativePathFromFile, @@ -280,7 +280,7 @@ function getNewStatementsAndRemoveFromOldFile( } const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreference(oldFile, preferences); + const quotePreference = getQuotePreferenceFromFile(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 7ad7bf4cc33..5bfc12b8ca8 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -23,6 +23,7 @@ import { DeclarationStatement, EmitHint, EmitTextWriter, + emptyArray, endsWith, Expression, factory, @@ -40,6 +41,7 @@ import { formatting, FunctionDeclaration, FunctionExpression, + FutureSourceFile, getAncestor, getFirstNonSpaceCharacterPosition, getFormatCodeSettingsForWriting, @@ -337,6 +339,12 @@ interface ChangeText extends BaseChange { readonly text: string; } +interface NewFileInsertion { + readonly fileName: string; + readonly oldFile?: SourceFile; + readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]; +} + function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange { return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; } @@ -480,7 +488,7 @@ export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): /** @internal */ export class ChangeTracker { private readonly changes: Change[] = []; - private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; + private newFileChanges?: NewFileInsertion[]; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; @@ -600,28 +608,42 @@ export class ChangeTracker { this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options); } - public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void { + public insertNodeAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNode: Statement, blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween); } - public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { + public insertNodesAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween); } - private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { - const pos = getInsertionPositionAtSourceFileTop(sourceFile); + private insertAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { + const pos = sourceFile.kind ? getInsertionPositionAtSourceFileTop(sourceFile) : 0; const options = { prefix: pos === 0 ? undefined : this.newLineCharacter, - suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), + suffix: (sourceFile.kind && isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), }; if (isArray(insert)) { - this.insertNodesAt(sourceFile, pos, insert, options); + if (sourceFile.kind) { + this.insertNodesAt(sourceFile, pos, insert, options); + } + else { + this.insertStatementsInNewFile(sourceFile.fileName, insert); + } } else { - this.insertNodeAt(sourceFile, pos, insert, options); + if (sourceFile.kind) { + this.insertNodeAt(sourceFile, pos, insert, options); + } + else { + this.insertStatementsInNewFile(sourceFile.fileName, [insert]); + } } } + private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { + (this.newFileChanges ??= []).push({ fileName, statements, oldFile }); + } + public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray, newParam: ParameterDeclaration): void { const p0 = firstOrUndefined(parameters); if (p0) { @@ -1128,14 +1150,14 @@ export class ChangeTracker { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); - for (const { oldFile, fileName, statements } of this.newFiles) { + for (const { oldFile, fileName, statements } of this.newFileChanges ?? emptyArray) { changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } return changes; } public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { - this.newFiles.push({ oldFile, fileName, statements }); + this.insertStatementsInNewFile(fileName, statements, oldFile); } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 7b29fd02dfb..d237e191103 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -85,11 +85,13 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, + FutureSourceFile, getAssignmentDeclarationKind, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, getEmitScriptTarget, getExternalModuleImportEqualsDeclarationExpression, + getImpliedNodeFormatForFile, getIndentString, getJSDocEnumTag, getLastChild, @@ -267,6 +269,8 @@ import { ModifierFlags, ModuleDeclaration, ModuleInstanceState, + ModuleKind, + ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierResolutionHost, moduleSpecifiers, @@ -350,6 +354,7 @@ import { textSpanEnd, Token, tokenToString, + toPath, tryCast, Type, TypeChecker, @@ -2472,9 +2477,14 @@ export function quotePreferenceFromString(str: StringLiteral, sourceFile: Source } /** @internal */ -export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { +export function getQuotePreference(preferences: UserPreferences): QuotePreference { + return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; +} + +/** @internal */ +export function getQuotePreferenceFromFile(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { if (preferences.quotePreference && preferences.quotePreference !== "auto") { - return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; + return getQuotePreference(preferences); } else { // ignore synthetic import added when importHelpers: true @@ -2561,17 +2571,18 @@ 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 existingImportStatements = sourceFile.kind ? filter(sourceFile.statements, importKindPredicate) : undefined; let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; - if (!existingImportStatements.length) { + if (!existingImportStatements?.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { + Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); @@ -2590,6 +2601,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So else { const lastExistingImport = lastOrUndefined(existingImportStatements); if (lastExistingImport) { + Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); changes.insertNodesAfter(sourceFile, lastExistingImport, sortedNewImports); } else { @@ -3322,7 +3334,7 @@ export function getContextualTypeFromParent(node: Expression, checker: TypeCheck /** @internal */ export function quote(sourceFile: SourceFile, preferences: UserPreferences, text: string): string { // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const quoted = JSON.stringify(text); return quotePreference === QuotePreference.Single ? `'${stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted; } @@ -3683,7 +3695,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); @@ -3783,7 +3795,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 (fromFile.kind && isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { if (usesNodeCoreModules === undefined) { usesNodeCoreModules = consumesNodeCoreModules(fromFile); } @@ -4041,8 +4053,8 @@ export function isDeprecatedDeclaration(decl: Declaration) { } /** @internal */ -export function shouldUseUriStyleNodeCoreModules(file: SourceFile, program: Program): boolean { - const decisionFromFile = firstDefined(file.imports, node => { +export function shouldUseUriStyleNodeCoreModules(file: SourceFile | FutureSourceFile, program: Program): boolean { + const decisionFromFile = file.kind && firstDefined(file.imports, node => { if (JsTyping.nodeCoreModules.has(node.text)) { return startsWith(node.text, "node:"); } @@ -4064,6 +4076,27 @@ export function diagnosticToString(diag: DiagnosticOrDiagnosticAndArguments): st : getLocaleSpecificMessage(diag); } +/** @internal */ +export function createFutureSourceFile( + fileName: string, + program: Program, + host: ModuleResolutionHost, + moduleSyntax?: ModuleKind.CommonJS | ModuleKind.ESNext, +): FutureSourceFile { + const path = toPath(fileName, /*basePath*/ undefined, program.getCanonicalFileName); + const impliedNodeFormat = getImpliedNodeFormatForFile(path, program.getPackageJsonInfoCache?.(), host, program.getCompilerOptions()); + if (moduleSyntax === ModuleKind.CommonJS && impliedNodeFormat === ModuleKind.ESNext) { + Debug.fail("ES Modules cannot contain CommonJS syntax"); + } + return { + fileName, + path, + commonJsModuleIndicator: moduleSyntax === ModuleKind.CommonJS, + externalModuleIndicator: moduleSyntax === ModuleKind.ESNext, + impliedNodeFormat, + }; +} + /** * Get format code settings for a code writing context (e.g. when formatting text changes or completions code). *