'Move to file' refactor (#53542)

This commit is contained in:
navya9singh
2023-04-21 11:48:53 -07:00
committed by GitHub
parent 09b1c55f83
commit d3bbef390d
48 changed files with 3437 additions and 910 deletions
+4
View File
@@ -7600,6 +7600,10 @@
"category": "Message",
"code": 95177
},
"Move to file": {
"category": "Message",
"code": 95178
},
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
+16 -5
View File
@@ -151,11 +151,18 @@ function getPreferences(
? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index]
: [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
}
const allowImportingTsExtension = shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.fileName);
switch (preferredEnding) {
case ModuleSpecifierEnding.JsExtension: return [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
case ModuleSpecifierEnding.JsExtension: return allowImportingTsExtension
? [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index]
: [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
case ModuleSpecifierEnding.TsExtension: return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index];
case ModuleSpecifierEnding.Index: return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
case ModuleSpecifierEnding.Minimal: return [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
case ModuleSpecifierEnding.Index: return allowImportingTsExtension
? [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension]
: [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
case ModuleSpecifierEnding.Minimal: return allowImportingTsExtension
? [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension]
: [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
default: Debug.assertNever(preferredEnding);
}
},
@@ -1046,7 +1053,12 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
return fileName;
}
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
const tsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.TsExtension);
if (fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Cts]) && tsPriority !== -1 && tsPriority < jsPriority) {
return fileName;
}
else if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
return noExtension + getJSExtensionForFile(fileName, options);
}
else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) {
@@ -1072,7 +1084,6 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
// know if a .d.ts extension is valid, so use no extension or a .js extension
if (isDeclarationFileName(fileName)) {
const extensionlessPriority = allowedEndings.findIndex(e => e === ModuleSpecifierEnding.Minimal || e === ModuleSpecifierEnding.Index);
const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
return extensionlessPriority !== -1 && extensionlessPriority < jsPriority
? noExtension
: noExtension + getJSExtensionForFile(fileName, options);
-1
View File
@@ -4239,7 +4239,6 @@ export interface SourceFileLike {
getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number;
}
/** @internal */
export interface RedirectInfo {
/** Source file this redirects to. */
+9 -1
View File
@@ -795,6 +795,14 @@ export class SessionClient implements LanguageService {
return response.body!; // TODO: GH#18217
}
getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string; files: string[]; } {
const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName);
const request = this.processRequest<protocol.GetMoveToRefactoringFileSuggestionsRequest>(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args);
const response = this.processResponse<protocol.GetMoveToRefactoringFileSuggestions>(request);
return { newFileName: response.body?.newFileName, files:response.body?.files }!;
}
getEditsForRefactor(
fileName: string,
_formatOptions: FormatCodeSettings,
@@ -818,7 +826,7 @@ export class SessionClient implements LanguageService {
const renameFilename: string | undefined = response.body.renameFilename;
let renameLocation: number | undefined;
if (renameFilename !== undefined) {
renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217
renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!);
}
return {
+16 -5
View File
@@ -3299,7 +3299,6 @@ export class TestState {
ts.Debug.fail(`Did not expect a change in ${change.fileName}`);
}
const oldText = this.tryGetFileContent(change.fileName);
ts.Debug.assert(!!change.isNewFile === (oldText === undefined));
const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges);
this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent);
}
@@ -3912,6 +3911,18 @@ export class TestState {
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
}
public moveToFile(options: FourSlashInterface.MoveToFileOptions): void {
assert(this.getRanges().length === 1, "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file");
const range = this.getRanges()[0];
const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }, /*triggerReason*/ undefined, /*kind*/ undefined, /*includeInteractiveActions*/ true), r => r.name === "Move to file")!;
assert(refactor.actions.length === 1);
const action = ts.first(refactor.actions);
assert(action.name === "Move to file" && action.description === "Move to file");
const editInfo = this.languageService.getEditsForRefactor(range.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions, options.interactiveRefactorArguments)!;
this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
}
private testNewFileContents(edits: readonly ts.FileTextChanges[], newFileContents: { [fileName: string]: string }, description: string): void {
for (const { fileName, textChanges } of edits) {
const newContent = newFileContents[fileName];
@@ -4217,11 +4228,11 @@ export class TestState {
private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
}
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line local/no-in-operator
private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] {
return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind, includeInteractiveActions); // eslint-disable-line local/no-in-operator
}
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): readonly ts.ApplicableRefactorInfo[] {
return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind, includeInteractiveActions) || ts.emptyArray;
}
public configurePlugin(pluginName: string, configuration: any): void {
+10
View File
@@ -610,6 +610,10 @@ export class Verify extends VerifyNegatable {
this.state.moveToNewFile(options);
}
public moveToFile(options: MoveToFileOptions): void {
this.state.moveToFile(options);
}
public noMoveToNewFile(): void {
this.state.noMoveToNewFile();
}
@@ -1896,6 +1900,12 @@ export interface MoveToNewFileOptions {
readonly preferences?: ts.UserPreferences;
}
export interface MoveToFileOptions {
readonly newFileContents: { readonly [fileName: string]: string };
readonly interactiveRefactorArguments: ts.InteractiveRefactorArguments;
readonly preferences?: ts.UserPreferences;
}
export type RenameLocationsOptions = readonly RenameLocationOptions[] | {
readonly findInStrings?: boolean;
readonly findInComments?: boolean;
+3
View File
@@ -616,6 +616,9 @@ class LanguageServiceShimProxy implements ts.LanguageService {
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
throw new Error("Not supported on the shim.");
}
getMoveToRefactoringFileSuggestions(): { newFileName: string, files: string[] } {
throw new Error("Not supported on the shim.");
}
organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] {
throw new Error("Not supported on the shim.");
}
+25
View File
@@ -4,6 +4,7 @@ import type {
EndOfLineState,
FileExtensionInfo,
HighlightSpanKind,
InteractiveRefactorArguments,
MapLike,
OutliningSpanKind,
OutputFile,
@@ -142,6 +143,7 @@ export const enum CommandTypes {
GetApplicableRefactors = "getApplicableRefactors",
GetEditsForRefactor = "getEditsForRefactor",
GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions",
/** @internal */
GetEditsForRefactorFull = "getEditsForRefactor-full",
@@ -606,6 +608,27 @@ export interface GetApplicableRefactorsResponse extends Response {
body?: ApplicableRefactorInfo[];
}
/**
* Request refactorings at a given position or selection area to move to an existing file.
*/
export interface GetMoveToRefactoringFileSuggestionsRequest extends Request {
command: CommandTypes.GetMoveToRefactoringFileSuggestions;
arguments: GetMoveToRefactoringFileSuggestionsRequestArgs;
}
export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & {
kind?: string;
};
/**
* Response is a list of available files.
* Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring
*/
export interface GetMoveToRefactoringFileSuggestions extends Response {
body: {
newFileName: string;
files: string[];
};
}
/**
* A set of one or more available refactoring actions, grouped under a parent refactoring.
*/
@@ -680,6 +703,8 @@ export type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & {
refactor: string;
/* The 'name' property from the refactoring action */
action: string;
/* Arguments for interactive action */
interactiveRefactorArguments?: InteractiveRefactorArguments;
};
+17 -4
View File
@@ -881,6 +881,7 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [
protocol.CommandTypes.ApplyCodeActionCommand,
protocol.CommandTypes.GetSupportedCodeFixes,
protocol.CommandTypes.GetApplicableRefactors,
protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
protocol.CommandTypes.GetEditsForRefactor,
protocol.CommandTypes.GetEditsForRefactorFull,
protocol.CommandTypes.OrganizeImports,
@@ -2687,6 +2688,7 @@ export class Session<TMessage = string> implements EventSender {
args.refactor,
args.action,
this.getPreferences(file),
args.interactiveRefactorArguments
);
if (result === undefined) {
@@ -2702,11 +2704,19 @@ export class Session<TMessage = string> implements EventSender {
const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!;
mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits);
}
return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) };
}
else {
return result;
return {
renameLocation: mappedRenameLocation,
renameFilename,
edits: this.mapTextChangesToCodeEdits(edits)
};
}
return result;
}
private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFileName: string, files: string[] }{
const { file, project } = this.getFileAndProject(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file)!;
return project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file));
}
private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] {
@@ -3429,6 +3439,9 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => {
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true));
},
[protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => {
return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments));
},
[protocol.CommandTypes.GetEditsForRefactorFull]: (request: protocol.GetEditsForRefactorRequest) => {
return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ false));
},
+1
View File
@@ -6,6 +6,7 @@ export * from "../refactors/convertImport";
export * from "../refactors/extractType";
export * from "../refactors/helpers";
export * from "../refactors/moveToNewFile";
export * from "../refactors/moveToFile";
import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction";
export { addOrRemoveBracesToArrowFunction };
import * as convertArrowFunctionOrFunctionExpression from "./ts.refactor.convertArrowFunctionOrFunctionExpression";
+3 -2
View File
@@ -2,6 +2,7 @@ import {
ApplicableRefactorInfo,
arrayFrom,
flatMapIterator,
InteractiveRefactorArguments,
Refactor,
RefactorContext,
RefactorEditInfo,
@@ -30,7 +31,7 @@ export function getApplicableRefactors(context: RefactorContext, includeInteract
}
/** @internal */
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined {
export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string, interactiveRefactorArguments?: InteractiveRefactorArguments): RefactorEditInfo | undefined {
const refactor = refactors.get(refactorName);
return refactor && refactor.getEditsForAction(context, actionName);
return refactor && refactor.getEditsForAction(context, actionName, interactiveRefactorArguments);
}
File diff suppressed because it is too large Load Diff
+34 -870
View File
@@ -1,135 +1,56 @@
import { getModuleSpecifier } from "../../compiler/moduleSpecifiers";
import {
AnyImportOrRequireStatement,
append,
ApplicableRefactorInfo,
AssignmentDeclarationKind,
BinaryExpression,
BindingElement,
BindingName,
CallExpression,
canHaveDecorators,
canHaveModifiers,
canHaveSymbol, cast,
ClassDeclaration,
codefix,
combinePaths,
concatenate,
contains,
copyEntries,
createModuleSpecifierResolutionHost,
createTextRangeFromSpan,
Debug,
Declaration,
DeclarationStatement,
Diagnostics,
emptyArray,
EnumDeclaration,
escapeLeadingUnderscores,
Expression,
ExpressionStatement,
extensionFromPath,
ExternalModuleReference,
factory,
find,
FindAllReferences,
findIndex,
firstDefined,
flatMap,
forEachEntry,
FunctionDeclaration,
getAssignmentDeclarationKind,
fileShouldUseJavaScriptRequire,
getBaseFileName,
GetCanonicalFileName,
getDecorators,
getDirectoryPath,
getLocaleSpecificMessage,
getModifiers,
getPropertySymbolFromBindingElement,
getQuotePreference,
getRangesWhere,
getRefactorContextSpan,
getRelativePathFromFile,
getSymbolId,
getUniqueName,
hasSyntacticModifier,
hostGetCanonicalFileName,
Identifier,
ImportDeclaration,
ImportEqualsDeclaration,
insertImports,
InterfaceDeclaration,
InternalSymbolName,
isArrayLiteralExpression,
isBinaryExpression,
isBindingElement,
isDeclarationName,
isExpressionStatement,
isExternalModuleReference,
isIdentifier,
isImportDeclaration,
isImportEqualsDeclaration,
isNamedDeclaration,
isObjectLiteralExpression,
isOmittedExpression,
isPrologueDirective,
isPropertyAccessExpression,
isPropertyAssignment,
isRequireCall,
isSourceFile,
isStringLiteral,
isStringLiteralLike,
isVariableDeclaration,
isVariableDeclarationList,
isVariableStatement,
LanguageServiceHost,
last,
length,
makeImportIfNecessary,
makeStringLiteral,
mapDefined,
ModifierFlags,
ModifierLike,
ModuleDeclaration,
NamedImportBindings,
Node,
NodeFlags,
nodeSeenTracker,
normalizePath,
ObjectBindingElementWithoutPropertyName,
Program,
PropertyAccessExpression,
PropertyAssignment,
QuotePreference,
rangeContainsRange,
RefactorContext,
RefactorEditInfo,
RequireOrImportCall,
RequireVariableStatement,
resolvePath,
ScriptTarget,
skipAlias,
some,
SourceFile,
Statement,
StringLiteralLike,
Symbol,
SymbolFlags,
symbolNameNoDefault,
SyntaxKind,
takeWhile,
textChanges,
TransformFlags,
tryCast,
TypeAliasDeclaration,
TypeChecker,
TypeNode,
UserPreferences,
VariableDeclaration,
VariableDeclarationList,
VariableStatement,
} from "../_namespaces/ts";
import { registerRefactor } from "../_namespaces/ts.refactor";
import {
addExports,
addExportToChanges,
addNewFileToTsconfig,
createNewFileName,
createOldFileImportsFromTargetFile,
deleteMovedStatements,
deleteUnusedOldImports,
filterImport,
forEachImportInStatement,
getStatementsToMove,
getTopLevelDeclarationStatement,
getUsageInfo,
isTopLevelDeclaration,
makeImportOrRequire,
moduleSpecifierFromImport,
nameOfTopLevelDeclaration,
registerRefactor,
SupportedImportStatement,
ToMove,
updateImportsInOtherFiles,
UsageInfo
} from "../_namespaces/ts.refactor";
const refactorName = "Move to a new file";
const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file);
@@ -156,55 +77,16 @@ registerRefactor(refactorName, {
getEditsForAction: function getRefactorEditsToMoveToNewFile(context, actionName): RefactorEditInfo {
Debug.assert(actionName === refactorName, "Wrong refactor invoked");
const statements = Debug.checkDefined(getStatementsToMove(context));
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences));
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences, context));
return { edits, renameFilename: undefined, renameLocation: undefined };
}
});
interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; }
function getRangeToMove(context: RefactorContext): RangeToMove | undefined {
const { file } = context;
const range = createTextRangeFromSpan(getRefactorContextSpan(context));
const { statements } = file;
const startNodeIndex = findIndex(statements, s => s.end > range.pos);
if (startNodeIndex === -1) return undefined;
const startStatement = statements[startNodeIndex];
if (isNamedDeclaration(startStatement) && startStatement.name && rangeContainsRange(startStatement.name, range)) {
return { toMove: [statements[startNodeIndex]], afterLast: statements[startNodeIndex + 1] };
}
// Can't only partially include the start node or be partially into the next node
if (range.pos > startStatement.getStart(file)) return undefined;
const afterEndNodeIndex = findIndex(statements, s => s.end > range.end, startNodeIndex);
// Can't be partially into the next node
if (afterEndNodeIndex !== -1 && (afterEndNodeIndex === 0 || statements[afterEndNodeIndex].getStart(file) < range.end)) return undefined;
return {
toMove: statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex),
afterLast: afterEndNodeIndex === -1 ? undefined : statements[afterEndNodeIndex],
};
}
function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void {
function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences, context: RefactorContext): void {
const checker = program.getTypeChecker();
const usage = getUsageInfo(oldFile, toMove.all, checker);
const currentDirectory = getDirectoryPath(oldFile.fileName);
const extension = extensionFromPath(oldFile.fileName);
const newFilename = combinePaths(
// new file is always placed in the same directory as the old file
currentDirectory,
// ensures the filename computed below isn't already taken
makeUniqueFilename(
// infers a name for the new file from the symbols being moved
inferNewFilename(usage.oldFileImportsFromNewFile, usage.movedSymbols),
extension,
currentDirectory,
host))
// new file has same extension as old file
+ extension;
const newFilename = createNewFileName(oldFile, program, context, host);
// If previous file was global, this is easy.
changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences));
@@ -212,76 +94,19 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes
addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host));
}
interface StatementRange {
readonly first: Statement;
readonly afterLast: Statement | undefined;
}
interface ToMove {
readonly all: readonly Statement[];
readonly ranges: readonly StatementRange[];
}
function getStatementsToMove(context: RefactorContext): ToMove | undefined {
const rangeToMove = getRangeToMove(context);
if (rangeToMove === undefined) return undefined;
const all: Statement[] = [];
const ranges: StatementRange[] = [];
const { toMove, afterLast } = rangeToMove;
getRangesWhere(toMove, isAllowedStatementToMove, (start, afterEndIndex) => {
for (let i = start; i < afterEndIndex; i++) all.push(toMove[i]);
ranges.push({ first: toMove[start], afterLast });
});
return all.length === 0 ? undefined : { all, ranges };
}
function isAllowedStatementToMove(statement: Statement): boolean {
// Filters imports and prologue directives out of the range of statements to move.
// Imports will be copied to the new file anyway, and may still be needed in the old file.
// Prologue directives will be copied to the new file and should be left in the old file.
return !isPureImport(statement) && !isPrologueDirective(statement);
}
function isPureImport(node: Node): boolean {
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
return true;
case SyntaxKind.ImportEqualsDeclaration:
return !hasSyntacticModifier(node, ModifierFlags.Export);
case SyntaxKind.VariableStatement:
return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*requireStringLiteralLikeArgument*/ true));
default:
return false;
}
}
function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void {
const cfg = program.getCompilerOptions().configFile;
if (!cfg) return;
const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension));
const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName);
const cfgObject = cfg.statements[0] && tryCast(cfg.statements[0].expression, isObjectLiteralExpression);
const filesProp = cfgObject && find(cfgObject.properties, (prop): prop is PropertyAssignment =>
isPropertyAssignment(prop) && isStringLiteral(prop.name) && prop.name.text === "files");
if (filesProp && isArrayLiteralExpression(filesProp.initializer)) {
changes.insertNodeInListAfter(cfg, last(filesProp.initializer.elements), factory.createStringLiteral(newFilePath), filesProp.initializer.elements);
}
}
function getNewStatementsAndRemoveFromOldFile(
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences,
) {
const checker = program.getTypeChecker();
const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective);
if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size() === 0) {
if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByTargetFile.size === 0) {
deleteMovedStatements(oldFile, toMove.ranges, changes);
return [...prologueDirectives, ...toMove.all];
}
const useEsModuleSyntax = !!oldFile.externalModuleIndicator;
const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(newFilename, program, host, !!oldFile.commonJsModuleIndicator);
const quotePreference = getQuotePreference(oldFile, preferences);
const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference);
const importsFromNewFile = createOldFileImportsFromTargetFile(oldFile, usage.oldFileImportsFromTargetFile, newFilename, program, host, useEsModuleSyntax, quotePreference);
if (importsFromNewFile) {
insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences);
}
@@ -290,8 +115,8 @@ function getNewStatementsAndRemoveFromOldFile(
deleteMovedStatements(oldFile, toMove.ranges, changes);
updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename, quotePreference);
const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference);
const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax);
const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference);
const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromTargetFile, useEsModuleSyntax);
if (imports.length && body.length) {
return [
...prologueDirectives,
@@ -308,295 +133,10 @@ function getNewStatementsAndRemoveFromOldFile(
];
}
function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) {
for (const { first, afterLast } of moved) {
changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast);
}
}
function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) {
for (const statement of oldFile.statements) {
if (contains(toMove, statement)) continue;
forEachImportInStatement(statement, i => deleteUnusedImports(oldFile, i, changes, name => toDelete.has(checker.getSymbolAtLocation(name)!)));
}
}
function updateImportsInOtherFiles(
changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newFilename: string, quotePreference: QuotePreference
): void {
const checker = program.getTypeChecker();
for (const sourceFile of program.getSourceFiles()) {
if (sourceFile === oldFile) continue;
for (const statement of sourceFile.statements) {
forEachImportInStatement(statement, importNode => {
if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return;
const shouldMove = (name: Identifier): boolean => {
const symbol = isBindingElement(name.parent)
? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName)
: skipAlias(checker.getSymbolAtLocation(name)!, checker); // TODO: GH#18217
return !!symbol && movedSymbols.has(symbol);
};
deleteUnusedImports(sourceFile, importNode, changes, shouldMove); // These will be changed to imports from the new file
const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), newFilename);
const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host));
const newImportDeclaration = filterImport(importNode, makeStringLiteral(newModuleSpecifier, quotePreference), shouldMove);
if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration);
const ns = getNamespaceLikeImport(importNode);
if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleSpecifier, ns, importNode, quotePreference);
});
}
}
}
function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined {
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ?
node.importClause.namedBindings.name : undefined;
case SyntaxKind.ImportEqualsDeclaration:
return node.name;
case SyntaxKind.VariableDeclaration:
return tryCast(node.name, isIdentifier);
default:
return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`);
}
}
function updateNamespaceLikeImport(
changes: textChanges.ChangeTracker,
sourceFile: SourceFile,
checker: TypeChecker,
movedSymbols: ReadonlySymbolSet,
newModuleSpecifier: string,
oldImportId: Identifier,
oldImportNode: SupportedImport,
quotePreference: QuotePreference
): void {
const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext);
let needUniqueName = false;
const toChange: Identifier[] = [];
FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => {
if (!isPropertyAccessExpression(ref.parent)) return;
needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true);
if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) {
toChange.push(ref);
}
});
if (toChange.length) {
const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName;
for (const ref of toChange) {
changes.replaceNode(sourceFile, ref, factory.createIdentifier(newNamespaceName));
}
changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, preferredNewNamespaceName, newModuleSpecifier, quotePreference));
}
}
function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node {
const newNamespaceId = factory.createIdentifier(newNamespaceName);
const newModuleString = makeStringLiteral(newModuleSpecifier, quotePreference);
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
return factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)),
newModuleString,
/*assertClause*/ undefined);
case SyntaxKind.ImportEqualsDeclaration:
return factory.createImportEqualsDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString));
case SyntaxKind.VariableDeclaration:
return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString));
default:
return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`);
}
}
function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike {
return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier
: i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression
: i.initializer.arguments[0]);
}
function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void {
if (isImportDeclaration(statement)) {
if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport);
}
else if (isImportEqualsDeclaration(statement)) {
if (isExternalModuleReference(statement.moduleReference) && isStringLiteralLike(statement.moduleReference.expression)) {
cb(statement as SupportedImport);
}
}
else if (isVariableStatement(statement)) {
for (const decl of statement.declarationList.declarations) {
if (decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true)) {
cb(decl as SupportedImport);
}
}
}
}
type SupportedImport =
| ImportDeclaration & { moduleSpecifier: StringLiteralLike }
| ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } }
| VariableDeclaration & { initializer: RequireOrImportCall };
type SupportedImportStatement =
| ImportDeclaration
| ImportEqualsDeclaration
| VariableStatement;
function createOldFileImportsFromNewFile(
sourceFile: SourceFile,
newFileNeedExport: ReadonlySymbolSet,
newFileNameWithExtension: string,
program: Program,
host: LanguageServiceHost,
useEs6Imports: boolean,
quotePreference: QuotePreference
): AnyImportOrRequireStatement | undefined {
let defaultImport: Identifier | undefined;
const imports: string[] = [];
newFileNeedExport.forEach(symbol => {
if (symbol.escapedName === InternalSymbolName.Default) {
defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217
}
else {
imports.push(symbol.name);
}
});
return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference);
}
function makeImportOrRequire(
sourceFile: SourceFile,
defaultImport: Identifier | undefined,
imports: readonly string[],
newFileNameWithExtension: string,
program: Program,
host: LanguageServiceHost,
useEs6Imports: boolean,
quotePreference: QuotePreference
): AnyImportOrRequireStatement | undefined {
const pathToNewFile = resolvePath(getDirectoryPath(sourceFile.path), newFileNameWithExtension);
const pathToNewFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFile, createModuleSpecifierResolutionHost(program, host));
if (useEs6Imports) {
const specifiers = imports.map(i => factory.createImportSpecifier(/*isTypeOnly*/ false, /*propertyName*/ undefined, factory.createIdentifier(i)));
return makeImportIfNecessary(defaultImport, specifiers, pathToNewFileWithCorrectExtension, quotePreference);
}
else {
Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module.
const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i));
return bindingElements.length
? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(makeStringLiteral(pathToNewFileWithCorrectExtension, quotePreference))) as RequireVariableStatement
: undefined;
}
}
function makeVariableStatement(name: BindingName, type: TypeNode | undefined, initializer: Expression | undefined, flags: NodeFlags = NodeFlags.Const) {
return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags));
}
function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression {
return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]);
}
function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] {
return flatMap(toMove, statement => {
if (isTopLevelDeclarationStatement(statement) &&
!isExported(sourceFile, statement, useEs6Exports) &&
forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol)))) {
const exports = addExport(statement, useEs6Exports);
if (exports) return exports;
}
return statement;
});
}
function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
switch (importDecl.kind) {
case SyntaxKind.ImportDeclaration:
deleteUnusedImportsInDeclaration(sourceFile, importDecl, changes, isUnused);
break;
case SyntaxKind.ImportEqualsDeclaration:
if (isUnused(importDecl.name)) {
changes.delete(sourceFile, importDecl);
}
break;
case SyntaxKind.VariableDeclaration:
deleteUnusedImportsInVariableDeclaration(sourceFile, importDecl, changes, isUnused);
break;
default:
Debug.assertNever(importDecl, `Unexpected import decl kind ${(importDecl as SupportedImport).kind}`);
}
}
function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: ImportDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void {
if (!importDecl.importClause) return;
const { name, namedBindings } = importDecl.importClause;
const defaultUnused = !name || isUnused(name);
const namedBindingsUnused = !namedBindings ||
(namedBindings.kind === SyntaxKind.NamespaceImport ? isUnused(namedBindings.name) : namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name)));
if (defaultUnused && namedBindingsUnused) {
changes.delete(sourceFile, importDecl);
}
else {
if (name && defaultUnused) {
changes.delete(sourceFile, name);
}
if (namedBindings) {
if (namedBindingsUnused) {
changes.replaceNode(
sourceFile,
importDecl.importClause,
factory.updateImportClause(importDecl.importClause, importDecl.importClause.isTypeOnly, name, /*namedBindings*/ undefined)
);
}
else if (namedBindings.kind === SyntaxKind.NamedImports) {
for (const element of namedBindings.elements) {
if (isUnused(element.name)) changes.delete(sourceFile, element);
}
}
}
}
}
function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) {
const { name } = varDecl;
switch (name.kind) {
case SyntaxKind.Identifier:
if (isUnused(name)) {
if (varDecl.initializer && isRequireCall(varDecl.initializer, /*requireStringLiteralLikeArgument*/ true)) {
changes.delete(sourceFile,
isVariableDeclarationList(varDecl.parent) && length(varDecl.parent.declarations) === 1 ? varDecl.parent.parent : varDecl);
}
else {
changes.delete(sourceFile, name);
}
}
break;
case SyntaxKind.ArrayBindingPattern:
break;
case SyntaxKind.ObjectBindingPattern:
if (name.elements.every(e => isIdentifier(e.name) && isUnused(e.name))) {
changes.delete(sourceFile,
isVariableDeclarationList(varDecl.parent) && varDecl.parent.declarations.length === 1 ? varDecl.parent.parent : varDecl);
}
else {
for (const element of name.elements) {
if (isIdentifier(element.name) && isUnused(element.name)) {
changes.delete(sourceFile, element.name);
}
}
}
break;
}
}
function getNewFileImportsAndAddExportInOldFile(
oldFile: SourceFile,
importsToCopy: ReadonlySymbolSet,
newFileImportsFromOldFile: ReadonlySymbolSet,
importsToCopy: Map<Symbol, boolean>,
newFileImportsFromOldFile: Set<Symbol>,
changes: textChanges.ChangeTracker,
checker: TypeChecker,
program: Program,
@@ -640,379 +180,3 @@ function getNewFileImportsAndAddExportInOldFile(
append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, getBaseFileName(oldFile.fileName), program, host, useEsModuleSyntax, quotePreference));
return copiedOldImports;
}
function makeUniqueFilename(proposedFilename: string, extension: string, inDirectory: string, host: LanguageServiceHost): string {
let newFilename = proposedFilename;
for (let i = 1; ; i++) {
const name = combinePaths(inDirectory, newFilename + extension);
if (!host.fileExists(name)) return newFilename;
newFilename = `${proposedFilename}.${i}`;
}
}
function inferNewFilename(importsFromNewFile: ReadonlySymbolSet, movedSymbols: ReadonlySymbolSet): string {
return importsFromNewFile.forEachEntry(symbolNameNoDefault) || movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile";
}
interface UsageInfo {
// Symbols whose declarations are moved from the old file to the new file.
readonly movedSymbols: ReadonlySymbolSet;
// Symbols declared in the old file that must be imported by the new file. (May not already be exported.)
readonly newFileImportsFromOldFile: ReadonlySymbolSet;
// Subset of movedSymbols that are still used elsewhere in the old file and must be imported back.
readonly oldFileImportsFromNewFile: ReadonlySymbolSet;
readonly oldImportsNeededByNewFile: ReadonlySymbolSet;
// Subset of oldImportsNeededByNewFile that are will no longer be used in the old file.
readonly unusedImportsFromOldFile: ReadonlySymbolSet;
}
function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo {
const movedSymbols = new SymbolSet();
const oldImportsNeededByNewFile = new SymbolSet();
const newFileImportsFromOldFile = new SymbolSet();
const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx));
const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx);
if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code)
oldImportsNeededByNewFile.add(jsxNamespaceSymbol);
}
for (const statement of toMove) {
forEachTopLevelDeclaration(statement, decl => {
movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here"));
});
}
for (const statement of toMove) {
forEachReference(statement, checker, symbol => {
if (!symbol.declarations) return;
for (const decl of symbol.declarations) {
if (isInImport(decl)) {
oldImportsNeededByNewFile.add(symbol);
}
else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) {
newFileImportsFromOldFile.add(symbol);
}
}
});
}
const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone();
const oldFileImportsFromNewFile = new SymbolSet();
for (const statement of oldFile.statements) {
if (contains(toMove, statement)) continue;
// jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile.
if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) {
unusedImportsFromOldFile.delete(jsxNamespaceSymbol);
}
forEachReference(statement, checker, symbol => {
if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol);
unusedImportsFromOldFile.delete(symbol);
});
}
return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile };
function getJsxNamespaceSymbol(containsJsx: Node | undefined) {
if (containsJsx === undefined) {
return undefined;
}
const jsxNamespace = checker.getJsxNamespace(containsJsx);
// Strictly speaking, this could resolve to a symbol other than the JSX namespace.
// This will produce erroneous output (probably, an incorrectly copied import) but
// is expected to be very rare and easily reversible.
const jsxNamespaceSymbol = checker.resolveName(jsxNamespace, containsJsx, SymbolFlags.Namespace, /*excludeGlobals*/ true);
return !!jsxNamespaceSymbol && some(jsxNamespaceSymbol.declarations, isInImport)
? jsxNamespaceSymbol
: undefined;
}
}
// Below should all be utilities
function isInImport(decl: Declaration) {
switch (decl.kind) {
case SyntaxKind.ImportEqualsDeclaration:
case SyntaxKind.ImportSpecifier:
case SyntaxKind.ImportClause:
case SyntaxKind.NamespaceImport:
return true;
case SyntaxKind.VariableDeclaration:
return isVariableDeclarationInImport(decl as VariableDeclaration);
case SyntaxKind.BindingElement:
return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent);
default:
return false;
}
}
function isVariableDeclarationInImport(decl: VariableDeclaration) {
return isSourceFile(decl.parent.parent.parent) &&
!!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true);
}
function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined {
switch (i.kind) {
case SyntaxKind.ImportDeclaration: {
const clause = i.importClause;
if (!clause) return undefined;
const defaultImport = clause.name && keep(clause.name) ? clause.name : undefined;
const namedBindings = clause.namedBindings && filterNamedBindings(clause.namedBindings, keep);
return defaultImport || namedBindings
? factory.createImportDeclaration(/*modifiers*/ undefined, factory.createImportClause(clause.isTypeOnly, defaultImport, namedBindings), moduleSpecifier, /*assertClause*/ undefined)
: undefined;
}
case SyntaxKind.ImportEqualsDeclaration:
return keep(i.name) ? i : undefined;
case SyntaxKind.VariableDeclaration: {
const name = filterBindingName(i.name, keep);
return name ? makeVariableStatement(name, i.type, createRequireCall(moduleSpecifier), i.parent.flags) : undefined;
}
default:
return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`);
}
}
function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined {
if (namedBindings.kind === SyntaxKind.NamespaceImport) {
return keep(namedBindings.name) ? namedBindings : undefined;
}
else {
const newElements = namedBindings.elements.filter(e => keep(e.name));
return newElements.length ? factory.createNamedImports(newElements) : undefined;
}
}
function filterBindingName(name: BindingName, keep: (name: Identifier) => boolean): BindingName | undefined {
switch (name.kind) {
case SyntaxKind.Identifier:
return keep(name) ? name : undefined;
case SyntaxKind.ArrayBindingPattern:
return name;
case SyntaxKind.ObjectBindingPattern: {
// We can't handle nested destructurings or property names well here, so just copy them all.
const newElements = name.elements.filter(prop => prop.propertyName || !isIdentifier(prop.name) || keep(prop.name));
return newElements.length ? factory.createObjectBindingPattern(newElements) : undefined;
}
}
}
function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) {
node.forEachChild(function cb(node) {
if (isIdentifier(node) && !isDeclarationName(node)) {
const sym = checker.getSymbolAtLocation(node);
if (sym) onReference(sym);
}
else {
node.forEachChild(cb);
}
});
}
interface ReadonlySymbolSet {
size(): number;
has(symbol: Symbol): boolean;
forEach(cb: (symbol: Symbol) => void): void;
forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined;
}
class SymbolSet implements ReadonlySymbolSet {
private map = new Map<string, Symbol>();
add(symbol: Symbol): void {
this.map.set(String(getSymbolId(symbol)), symbol);
}
has(symbol: Symbol): boolean {
return this.map.has(String(getSymbolId(symbol)));
}
delete(symbol: Symbol): void {
this.map.delete(String(getSymbolId(symbol)));
}
forEach(cb: (symbol: Symbol) => void): void {
this.map.forEach(cb);
}
forEachEntry<T>(cb: (symbol: Symbol) => T | undefined): T | undefined {
return forEachEntry(this.map, cb);
}
clone(): SymbolSet {
const clone = new SymbolSet();
copyEntries(this.map, clone.map);
return clone;
}
size() {
return this.map.size;
}
}
type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...'
type NonVariableTopLevelDeclaration =
| FunctionDeclaration
| ClassDeclaration
| EnumDeclaration
| TypeAliasDeclaration
| InterfaceDeclaration
| ModuleDeclaration
| TopLevelExpressionStatement
| ImportEqualsDeclaration;
type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement;
interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; }
type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement;
function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration {
return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent);
}
function sourceFileOfTopLevelDeclaration(node: TopLevelDeclaration): Node {
return isVariableDeclaration(node) ? node.parent.parent.parent : node.parent;
}
function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement {
Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile");
return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node);
}
function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration {
switch (node.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ModuleDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.ImportEqualsDeclaration:
return true;
default:
return false;
}
}
function forEachTopLevelDeclaration<T>(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined {
switch (statement.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.ModuleDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.ImportEqualsDeclaration:
return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | ImportEqualsDeclaration);
case SyntaxKind.VariableStatement:
return firstDefined((statement as VariableStatement).declarationList.declarations, decl => forEachTopLevelDeclarationInBindingName(decl.name, cb));
case SyntaxKind.ExpressionStatement: {
const { expression } = statement as ExpressionStatement;
return isBinaryExpression(expression) && getAssignmentDeclarationKind(expression) === AssignmentDeclarationKind.ExportsProperty
? cb(statement as TopLevelExpressionStatement)
: undefined;
}
}
}
function forEachTopLevelDeclarationInBindingName<T>(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | undefined {
switch (name.kind) {
case SyntaxKind.Identifier:
return cb(cast(name.parent, (x): x is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(x) || isBindingElement(x)));
case SyntaxKind.ArrayBindingPattern:
case SyntaxKind.ObjectBindingPattern:
return firstDefined(name.elements, em => isOmittedExpression(em) ? undefined : forEachTopLevelDeclarationInBindingName(em.name, cb));
default:
return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`);
}
}
function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined {
return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier);
}
function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement {
switch (d.kind) {
case SyntaxKind.VariableDeclaration:
return d.parent.parent;
case SyntaxKind.BindingElement:
return getTopLevelDeclarationStatement(
cast(d.parent.parent, (p): p is TopLevelVariableDeclaration | BindingElement => isVariableDeclaration(p) || isBindingElement(p)));
default:
return d;
}
}
function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void {
if (isExported(sourceFile, decl, useEs6Exports, name)) return;
if (useEs6Exports) {
if (!isExpressionStatement(decl)) changes.insertExportModifier(sourceFile, decl);
}
else {
const names = getNamesToExportInCommonJS(decl);
if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment));
}
}
function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean {
if (useEs6Exports) {
return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol.exports?.has(name.escapedText));
}
return !!sourceFile.symbol && !!sourceFile.symbol.exports &&
getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name)));
}
function addExport(decl: TopLevelDeclarationStatement, useEs6Exports: boolean): readonly Statement[] | undefined {
return useEs6Exports ? [addEs6Export(decl)] : addCommonjsExport(decl);
}
function addEs6Export(d: TopLevelDeclarationStatement): TopLevelDeclarationStatement {
const modifiers = canHaveModifiers(d) ? concatenate([factory.createModifier(SyntaxKind.ExportKeyword)], getModifiers(d)) : undefined;
switch (d.kind) {
case SyntaxKind.FunctionDeclaration:
return factory.updateFunctionDeclaration(d, modifiers, d.asteriskToken, d.name, d.typeParameters, d.parameters, d.type, d.body);
case SyntaxKind.ClassDeclaration:
const decorators = canHaveDecorators(d) ? getDecorators(d) : undefined;
return factory.updateClassDeclaration(d, concatenate<ModifierLike>(decorators, modifiers), d.name, d.typeParameters, d.heritageClauses, d.members);
case SyntaxKind.VariableStatement:
return factory.updateVariableStatement(d, modifiers, d.declarationList);
case SyntaxKind.ModuleDeclaration:
return factory.updateModuleDeclaration(d, modifiers, d.name, d.body);
case SyntaxKind.EnumDeclaration:
return factory.updateEnumDeclaration(d, modifiers, d.name, d.members);
case SyntaxKind.TypeAliasDeclaration:
return factory.updateTypeAliasDeclaration(d, modifiers, d.name, d.typeParameters, d.type);
case SyntaxKind.InterfaceDeclaration:
return factory.updateInterfaceDeclaration(d, modifiers, d.name, d.typeParameters, d.heritageClauses, d.members);
case SyntaxKind.ImportEqualsDeclaration:
return factory.updateImportEqualsDeclaration(d, modifiers, d.isTypeOnly, d.name, d.moduleReference);
case SyntaxKind.ExpressionStatement:
return Debug.fail(); // Shouldn't try to add 'export' keyword to `exports.x = ...`
default:
return Debug.assertNever(d, `Unexpected declaration kind ${(d as DeclarationStatement).kind}`);
}
}
function addCommonjsExport(decl: TopLevelDeclarationStatement): readonly Statement[] | undefined {
return [decl, ...getNamesToExportInCommonJS(decl).map(createExportAssignment)];
}
function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] {
switch (decl.kind) {
case SyntaxKind.FunctionDeclaration:
case SyntaxKind.ClassDeclaration:
return [decl.name!.text]; // TODO: GH#18217
case SyntaxKind.VariableStatement:
return mapDefined(decl.declarationList.declarations, d => isIdentifier(d.name) ? d.name.text : undefined);
case SyntaxKind.ModuleDeclaration:
case SyntaxKind.EnumDeclaration:
case SyntaxKind.TypeAliasDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.ImportEqualsDeclaration:
return emptyArray;
case SyntaxKind.ExpressionStatement:
return Debug.fail("Can't export an ExpressionStatement"); // Shouldn't try to add 'export' keyword to `exports.x = ...`
default:
return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`);
}
}
/** Creates `exports.x = x;` */
function createExportAssignment(name: string): Statement {
return factory.createExpressionStatement(
factory.createBinaryExpression(
factory.createPropertyAccessExpression(factory.createIdentifier("exports"), factory.createIdentifier(name)),
SyntaxKind.EqualsToken,
factory.createIdentifier(name)));
}
+22 -1
View File
@@ -65,6 +65,8 @@ import {
EntityName,
equateValues,
ExportDeclaration,
Extension,
extensionFromPath,
FileReference,
FileTextChanges,
filter,
@@ -86,6 +88,7 @@ import {
getAdjustedRenameLocation,
getAllSuperTypeNodes,
getAssignmentDeclarationKind,
getBaseFileName,
GetCompletionsAtPositionOptions,
getContainerNode,
getDefaultLibFileName,
@@ -135,6 +138,7 @@ import {
InlayHints,
InlayHintsContext,
insertSorted,
InteractiveRefactorArguments,
InterfaceType,
IntersectionType,
isArray,
@@ -277,6 +281,7 @@ import {
SourceFile,
SourceFileLike,
SourceMapSource,
startsWith,
Statement,
stringContains,
StringLiteral,
@@ -320,6 +325,7 @@ import {
} from "./_namespaces/ts";
import * as NavigateTo from "./_namespaces/ts.NavigateTo";
import * as NavigationBar from "./_namespaces/ts.NavigationBar";
import { createNewFileName } from "./_namespaces/ts.refactor";
import * as classifier from "./classifier";
import * as classifier2020 from "./classifier2020";
@@ -2990,6 +2996,19 @@ export function createLanguageService(
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind), includeInteractiveActions);
}
function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFileName: string, files: string[] } {
synchronizeHostData();
const sourceFile = getValidSourceFile(fileName);
const allFiles = Debug.checkDefined(program.getSourceFiles());
const extension = extensionFromPath(fileName);
const files = mapDefined(allFiles, file => !program?.isSourceFileFromExternalLibrary(sourceFile) &&
!(sourceFile === getValidSourceFile(file.fileName) || extension === Extension.Ts && extensionFromPath(file.fileName) === Extension.Dts || extension === Extension.Dts && startsWith(getBaseFileName(file.fileName), "lib.") && extensionFromPath(file.fileName) === Extension.Dts)
&& extension === extensionFromPath(file.fileName) ? file.fileName : undefined);
const newFileName = createNewFileName(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host);
return { newFileName, files };
}
function getEditsForRefactor(
fileName: string,
formatOptions: FormatCodeSettings,
@@ -2997,10 +3016,11 @@ export function createLanguageService(
refactorName: string,
actionName: string,
preferences: UserPreferences = emptyOptions,
interactiveRefactorArguments?: InteractiveRefactorArguments,
): RefactorEditInfo | undefined {
synchronizeHostData();
const file = getValidSourceFile(fileName);
return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName);
return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName, interactiveRefactorArguments);
}
function toLineColumnOffset(fileName: string, position: number): LineAndCharacter {
@@ -3097,6 +3117,7 @@ export function createLanguageService(
updateIsDefinitionOfReferencedSymbols,
getApplicableRefactors,
getEditsForRefactor,
getMoveToRefactoringFileSuggestions,
toLineColumnOffset,
getSourceMapper: () => sourceMapper,
clearSourceMapperCache: () => sourceMapper.clearCache(),
+24 -14
View File
@@ -10,6 +10,7 @@ import {
concatenate,
ConstructorDeclaration,
contains,
createMultiMap,
createNodeFactory,
createPrinter,
createRange,
@@ -116,6 +117,7 @@ import {
mapDefined,
MethodSignature,
Modifier,
MultiMap,
NamedImportBindings,
NamedImports,
NamespaceImport,
@@ -337,6 +339,11 @@ interface ChangeText extends BaseChange {
readonly text: string;
}
interface NewFileInsertion {
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 +487,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?: MultiMap<string, NewFileInsertion> ;
private readonly classesWithNodesInsertedAtStart = new Map<number, { readonly node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, readonly sourceFile: SourceFile }>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];
@@ -622,6 +629,13 @@ export class ChangeTracker {
}
}
private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void {
if (!this.newFileChanges) {
this.newFileChanges = createMultiMap<string, NewFileInsertion>();
}
this.newFileChanges.add(fileName, { oldFile, statements });
}
public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray<ParameterDeclaration>, newParam: ParameterDeclaration): void {
const p0 = firstOrUndefined(parameters);
if (p0) {
@@ -1128,14 +1142,16 @@ 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) {
changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext));
if (this.newFileChanges) {
this.newFileChanges.forEach((insertions, fileName) => {
changes.push(changesToText.newFileChanges(fileName, insertions, 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);
}
}
@@ -1207,11 +1223,6 @@ function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaratio
/** @internal */
export type ValidateNonFormattedText = (node: Node, text: string) => void;
/** @internal */
export function getNewFileText(statements: readonly Statement[], scriptKind: ScriptKind, newLineCharacter: string, formatContext: formatting.FormatContext): string {
return changesToText.newFileChangesWorker(/*oldFile*/ undefined, scriptKind, statements, newLineCharacter, formatContext);
}
namespace changesToText {
export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] {
return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => {
@@ -1228,7 +1239,6 @@ namespace changesToText {
const textChanges = mapDefined(normalized, c => {
const span = createTextSpanFromRange(c.range);
const newText = computeNewText(c, sourceFile, newLineCharacter, formatContext, validate);
// Filter out redundant changes.
if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) {
return undefined;
@@ -1241,14 +1251,14 @@ namespace changesToText {
});
}
export function newFileChanges(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext);
export function newFileChanges(fileName: string, insertions: readonly NewFileInsertion[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges {
const text = newFileChangesWorker(getScriptKindFromFileName(fileName), insertions, newLineCharacter, formatContext);
return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true };
}
export function newFileChangesWorker(oldFile: SourceFile | undefined, scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
export function newFileChangesWorker(scriptKind: ScriptKind, insertions: readonly NewFileInsertion[], newLineCharacter: string, formatContext: formatting.FormatContext): string {
// TODO: this emits the file, parses it back, then formats it that -- may be a less roundabout way to do this
const nonFormattedText = statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, oldFile, newLineCharacter).text).join(newLineCharacter);
const nonFormattedText = flatMap(insertions, insertion => insertion.statements.map(s => s === SyntaxKind.NewLineTrivia ? "" : getNonformattedText(s, insertion.oldFile, newLineCharacter).text)).join(newLineCharacter);
const sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind);
const changes = formatting.formatDocument(sourceFile, formatContext);
return applyChanges(nonFormattedText, changes) + newLineCharacter;
+8 -3
View File
@@ -652,7 +652,8 @@ export interface LanguageService {
* arguments for any interactive action before offering it.
*/
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string, includeInteractiveActions?: boolean): ApplicableRefactorInfo[];
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined;
getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined, includeInteractiveActions?: InteractiveRefactorArguments): RefactorEditInfo | undefined;
getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFileName: string, files: string[] };
organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[];
@@ -1293,6 +1294,10 @@ export interface DocCommentTemplateOptions {
readonly generateReturnInDocTemplate?: boolean;
}
export interface InteractiveRefactorArguments {
targetFile: string;
}
export interface SignatureHelpParameter {
name: string;
documentation: SymbolDisplayPart[];
@@ -1785,10 +1790,10 @@ export interface Refactor {
kinds?: string[];
/** Compute the associated code actions */
getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined;
getEditsForAction(context: RefactorContext, actionName: string, interactiveRefactorArguments?: InteractiveRefactorArguments): RefactorEditInfo | undefined;
/** Compute (quickly) which actions are available here */
getAvailableActions(context: RefactorContext, includeInteractive?: boolean): readonly ApplicableRefactorInfo[];
getAvailableActions(context: RefactorContext, includeInteractive?: boolean, interactiveRefactorArguments?: InteractiveRefactorArguments): readonly ApplicableRefactorInfo[];
}
/** @internal */
+55 -1
View File
@@ -54,6 +54,7 @@ import {
ElementAccessExpression,
EmitFlags,
EmitHint,
emitModuleKindIsNonNodeESM,
emptyArray,
EndOfFileToken,
endsWith,
@@ -88,8 +89,10 @@ import {
getAssignmentDeclarationKind,
getCombinedNodeFlagsAlwaysIncludeJSDoc,
getDirectoryPath,
getEmitModuleKind,
getEmitScriptTarget,
getExternalModuleImportEqualsDeclarationExpression,
getImpliedNodeFormatForFile,
getIndentString,
getJSDocEnumTag,
getLastChild,
@@ -108,8 +111,10 @@ import {
getTextOfIdentifierOrLiteral,
getTextOfNode,
getTypesPackageName,
hasJSFileExtension,
hasSyntacticModifier,
HeritageClause,
hostGetCanonicalFileName,
Identifier,
identifierIsThisKeyword,
identity,
@@ -267,6 +272,7 @@ import {
ModifierFlags,
ModuleDeclaration,
ModuleInstanceState,
ModuleKind,
ModuleResolutionKind,
ModuleSpecifierResolutionHost,
moduleSpecifiers,
@@ -350,6 +356,7 @@ import {
textSpanEnd,
Token,
tokenToString,
toPath,
tryCast,
Type,
TypeChecker,
@@ -3137,7 +3144,12 @@ export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T>, in
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia?: boolean): NodeArray<T> | undefined;
/** @internal */
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined, includeTrivia = true): NodeArray<T> | undefined {
return nodes && factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma);
if (nodes) {
const cloned = factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma);
setTextRange(cloned, nodes);
return cloned;
}
return nodes;
}
/** @internal */
@@ -4165,3 +4177,45 @@ export function newCaseClauseTracker(checker: TypeChecker, clauses: readonly (Ca
}
}
}
/** @internal */
export function fileShouldUseJavaScriptRequire(file: SourceFile | string, program: Program, host: LanguageServiceHost, preferRequire?: boolean) {
const fileName = typeof file === "string" ? file : file.fileName;
if (!hasJSFileExtension(fileName)) {
return false;
}
const compilerOptions = program.getCompilerOptions();
const moduleKind = getEmitModuleKind(compilerOptions);
const impliedNodeFormat = typeof file === "string"
? getImpliedNodeFormatForFile(toPath(file, host.getCurrentDirectory(), hostGetCanonicalFileName(host)), program.getPackageJsonInfoCache?.(), host, compilerOptions)
: file.impliedNodeFormat;
if (impliedNodeFormat === ModuleKind.ESNext) {
return false;
}
if (impliedNodeFormat === ModuleKind.CommonJS) {
// Since we're in a JS file, assume the user is writing the JS that will run
// (i.e., assume `noEmit`), so a CJS-format file should just have require
// syntax, rather than imports that will be downleveled to `require`.
return true;
}
if (compilerOptions.verbatimModuleSyntax && moduleKind === ModuleKind.CommonJS) {
// Using ESM syntax under these options would result in an error.
return true;
}
if (compilerOptions.verbatimModuleSyntax && emitModuleKindIsNonNodeESM(moduleKind)) {
return false;
}
// impliedNodeFormat is undefined and `verbatimModuleSyntax` is off (or in an invalid combo)
// Use heuristics from existing code
if (typeof file === "object") {
if (file.commonJsModuleIndicator) {
return true;
}
if (file.externalModuleIndicator) {
return false;
}
}
return preferRequire;
}
+1
View File
@@ -199,3 +199,4 @@ import "./unittests/tsserver/versionCache";
import "./unittests/tsserver/watchEnvironment";
import "./unittests/debugDeprecation";
import "./unittests/tsserver/inconsistentErrorInEditor";
import "./unittests/tsserver/getMoveToRefactoringFileSuggestions";
@@ -0,0 +1,117 @@
import * as ts from "../../_namespaces/ts";
import {
baselineTsserverLogs,
createLoggerWithInMemoryLogs,
createSession,
openFilesForSession
} from "../helpers/tsserver";
import {
createServerHost,
File
} from "../helpers/virtualFileSystemWithWatch";
describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => {
it("works for suggesting a list of files, excluding node_modules within a project", () => {
const file1: File = {
path: "/project/a/file1.ts",
content: `interface ka {
name: string;
}
`
};
const file2: File = { path: "/project/b/file2.ts", content: "" };
const file3: File = { path: "/project/d/e/file3.ts", content: "" };
const file4: File = {
path: "/project/a/file4.ts",
content: `import { value } from "../node_modules/@types/node/someFile.d.ts";
import { value1 } from "../node_modules/.cache/someFile.d.ts";`
};
const nodeModulesFile1: File = {
path: "project/node_modules/@types/node/someFile.d.ts",
content: `export const value = 0;`
};
const nodeModulesFile2: File = {
path: "project/node_modules/.cache/someFile.d.ts",
content: `export const value1 = 0;`
};
const tsconfig: File = {
path: "/project/tsconfig.json",
content: "{}",
};
const host = createServerHost([file1, file2, file3, file3, file4, nodeModulesFile1, nodeModulesFile2, tsconfig]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([file1], session);
session.executeCommandSeq<ts.server.protocol.GetMoveToRefactoringFileSuggestionsRequest>({
command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
arguments: { file: file1.path, line: 1, offset: 11 }
});
baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, excluding node_modules within a project", session);
});
it("suggests only .ts file for a .ts filepath", () => {
const file1: File = {
path: "/file1.ts",
content: `interface ka {
name: string;
}
`
};
const file2: File = { path: "/file2.tsx", content: "" };
const file3: File = { path: "/file3.mts", content: "" };
const file4: File = { path: "/file4.cts", content: "" };
const file5: File = { path: "/file5.js", content: "" };
const file6: File = { path: "/file6.d.ts", content: "" };
const file7: File = { path: "/file7.ts", content: "" };
const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file2.tsx", "./file3.mts", "./file4.cts", "./file5.js", "./file6.d.ts", "./file7.ts"] }) };
const host = createServerHost([file1, file2, file3, file4, file5, file6, file7, tsconfig]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([file1], session);
session.executeCommandSeq<ts.server.protocol.GetMoveToRefactoringFileSuggestionsRequest>({
command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
arguments: { file: file1.path, line: 1, offset: 11 }
});
baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "suggests only .ts file for a .ts filepath", session);
});
it("suggests only .js file for a .js filepath", () => {
const file1: File = {
path: "/file1.js",
content: `class C {}`
};
const file2: File = { path: "/file2.js", content: "" };
const file3: File = { path: "/file3.mts", content: "" };
const file4: File = { path: "/file4.ts", content: "" };
const file5: File = { path: "/file5.js", content: "" };
const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file2.js", "./file3.mts", "./file4.ts", "./file5.js"] }) };
const host = createServerHost([file1, file2, file3, file4, file5, tsconfig]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([file1], session);
session.executeCommandSeq<ts.server.protocol.GetMoveToRefactoringFileSuggestionsRequest>({
command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
arguments: { file: file1.path, line: 1, offset: 7 }
});
baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "suggests only .js file for a .js filepath", session);
});
it("skips lib.d.ts files", () => {
const file1: File = {
path: "/file1.d.ts",
content: `class C {}`
};
const file2: File = { path: "/a/lib.d.ts", content: "" };
const file3: File = { path: "/a/file3.d.ts", content: "" };
const file4: File = { path: "/a/lib.es6.d.ts", content: "" };
const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.d.ts", "./a/lib.d.ts", "./a/file3.d.ts", "/a/lib.es6.d.ts"] }) };
const host = createServerHost([file1, file2, file3, file4, tsconfig]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([file1], session);
session.executeCommandSeq<ts.server.protocol.GetMoveToRefactoringFileSuggestionsRequest>({
command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions,
arguments: { file: file1.path, line: 1, offset: 7 }
});
baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "skips lib.d.ts files", session);
});
});
@@ -94,4 +94,30 @@ describe("unittests:: tsserver:: refactors", () => {
});
baselineTsserverLogs("refactors", "handles canonicalization of tsconfig path", session);
});
it("handles moving statement to an existing file", () => {
const aTs: File = { path: "/Foo/a.ts", content: "const x = 0;" };
const bTs: File = {
path: "/Foo/b.ts", content: `import {} from "./bar";
const a = 1;`};
const tsconfig: File = { path: "/Foo/tsconfig.json", content: `{ "files": ["./a.ts", "./b.ts"] }` };
const host = createServerHost([aTs, bTs, tsconfig]);
const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
openFilesForSession([aTs], session);
session.executeCommandSeq<ts.server.protocol.GetEditsForRefactorRequest>({
command: ts.server.protocol.CommandTypes.GetEditsForRefactor,
arguments: {
file: aTs.path,
startLine: 1,
startOffset: 1,
endLine: 2,
endOffset: aTs.content.length,
refactor: "Move to file",
action: "Move to file",
interactiveRefactorArguments: { targetFile: "/Foo/b.ts" },
}
});
baselineTsserverLogs("refactors", "handles moving statement to an existing file", session);
});
});