mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
'Move to file' refactor (#53542)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4239,7 +4239,6 @@ export interface SourceFileLike {
|
||||
getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
export interface RedirectInfo {
|
||||
/** Source file this redirects to. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user