From 17d1a6b624929bcbca2decb47f834c740cd5b088 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 7 Mar 2023 09:47:56 -0800 Subject: [PATCH 01/42] tests for moce to another file --- tests/cases/fourslash/moveToAnotherFile.ts | 38 +++++++++++++++++++ tests/cases/fourslash/moveToAnotherFile2.ts | 41 +++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/cases/fourslash/moveToAnotherFile.ts create mode 100644 tests/cases/fourslash/moveToAnotherFile2.ts diff --git a/tests/cases/fourslash/moveToAnotherFile.ts b/tests/cases/fourslash/moveToAnotherFile.ts new file mode 100644 index 0000000000000..f6bd20fe7239b --- /dev/null +++ b/tests/cases/fourslash/moveToAnotherFile.ts @@ -0,0 +1,38 @@ +/// + +//@Filename: /bar.ts +//// + +// @Filename: /a.ts +////// header comment +//// +////import './foo'; +////import { a, b, alreadyUnused } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] +////a; y; + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import { y } from './bar'; +import './foo'; +import { a, alreadyUnused } from './other'; +export const p = 0; +a; y;`, + + "/bar.ts": +`import { b } from './other'; +import { p } from './a'; + +export const y: Date = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToAnotherFile2.ts b/tests/cases/fourslash/moveToAnotherFile2.ts new file mode 100644 index 0000000000000..a08f2b02f43a3 --- /dev/null +++ b/tests/cases/fourslash/moveToAnotherFile2.ts @@ -0,0 +1,41 @@ +/// + +//@Filename: /bar.ts +////import './blah'; +////import './blah2'; + +// @Filename: /a.ts +////// header comment +//// +////import './foo'; +////import { a, b, alreadyUnused } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] +////a; y; + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import { y } from './bar'; +import './foo'; +import { a, alreadyUnused } from './other'; +export const p = 0; +a; y;`, + + "/bar.ts": +`import { p } from './a'; +import './blah'; +import './blah2'; +import { b } from './other'; + +export const y: Date = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); From 98bfb653767a9d521b68b2d2aa1783804a8a4ce7 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 7 Mar 2023 09:48:36 -0800 Subject: [PATCH 02/42] Works in moving to an existing blank file --- src/compiler/diagnosticMessages.json | 4 + src/harness/client.ts | 26 + src/harness/fourslashImpl.ts | 14 +- src/harness/fourslashInterfaceImpl.ts | 10 + src/harness/harnessLanguageService.ts | 3 + src/server/session.ts | 33 + src/services/_namespaces/ts.refactor.ts | 1 + src/services/refactorProvider.ts | 7 + src/services/refactors/moveToAnotherFile.ts | 982 ++++++++++++++++++++ src/services/services.ts | 15 + src/services/textChanges.ts | 12 + src/services/types.ts | 3 +- tests/cases/fourslash/fourslash.ts | 6 +- 13 files changed, 1113 insertions(+), 3 deletions(-) create mode 100644 src/services/refactors/moveToAnotherFile.ts diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 02620ab7620b1..3091e94e600fa 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7552,6 +7552,10 @@ "category": "Message", "code": 95175 }, + "Move to another file": { + "category": "Message", + "code": 95176 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/harness/client.ts b/src/harness/client.ts index 1b52ef2abe2f6..5c0eb6ee00b20 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -835,6 +835,32 @@ export class SessionClient implements LanguageService { }; } + getEditsForMoveToFileRefactor(fileName: string, _newFile: string, _formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, _preferences: UserPreferences | undefined): RefactorEditInfo { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; + args.refactor = refactorName; + args.action = actionName; + + const request = this.processRequest(protocol.CommandTypes.GetEditsForRefactor, args); + const response = this.processResponse(request); + + if (!response.body) { + return { edits: [], renameFilename: undefined, renameLocation: undefined }; + } + + const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); + + const renameFilename: string | undefined = response.body.renameFilename; + let renameLocation: number | undefined; + if (renameFilename !== undefined) { + renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 + } + return { + edits, + renameFilename, + renameLocation + }; + } + organizeImports(_args: OrganizeImportsArgs, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { return notImplemented(); } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 755f5c648b6e7..312142c6166d2 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3174,7 +3174,7 @@ 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)); + //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); } @@ -3873,6 +3873,18 @@ export class TestState { this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits); } + public moveToAnotherFile(options: FourSlashInterface.MoveToAnotherFileOptions): 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 }), r => r.name === "Move to another file")!; + assert(refactor.actions.length === 1); + const action = ts.first(refactor.actions); + assert(action.name === "Move to another file" && action.description === "Move to another file"); + + const editInfo = this.languageService.getEditsForMoveToFileRefactor(range.fileName, options.newFile, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions)!; + 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]; diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 4cf5c58d542b7..ba7517389470a 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -638,6 +638,10 @@ export class Verify extends VerifyNegatable { this.state.moveToNewFile(options); } + public moveToAnotherFile(options: MoveToAnotherFileOptions): void { + this.state.moveToAnotherFile(options); + } + public noMoveToNewFile(): void { this.state.noMoveToNewFile(); } @@ -1920,6 +1924,12 @@ export interface MoveToNewFileOptions { readonly preferences?: ts.UserPreferences; } +export interface MoveToAnotherFileOptions { + readonly newFileContents: { readonly [fileName: string]: string }; + readonly newFile: string; + readonly preferences?: ts.UserPreferences; +} + export type RenameLocationsOptions = readonly RenameLocationOptions[] | { readonly findInStrings?: boolean; readonly findInComments?: boolean; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b774bf35e622f..055ab2fc50be8 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -613,6 +613,9 @@ class LanguageServiceShimProxy implements ts.LanguageService { getEditsForRefactor(): ts.RefactorEditInfo { throw new Error("Not supported on the shim."); } + getEditsForMoveToFileRefactor(): ts.RefactorEditInfo { + throw new Error("Not supported on the shim."); + } getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } diff --git a/src/server/session.ts b/src/server/session.ts index 86b61f58e97e6..4dc8b08489ebc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2712,6 +2712,39 @@ export class Session implements EventSender { return result; } } +//@ts-ignore + private getEditsForMoveToFileRefactor(args: protocol.GetEditsForRefactorRequestArgs, newFile: string, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { + const { file, project } = this.getFileAndProject(args); + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const result = project.getLanguageService().getEditsForMoveToFileRefactor( + file, + newFile, + this.getFormatOptions(file), + this.extractPositionOrRange(args, scriptInfo), + "Move to another file", //args.refactor="Move to another file" + "Move to another file",//"args.action =Move to another file" + this.getPreferences(file), + ); + + if (result === undefined) { + return { + edits: [] + }; + } + + if (simplifiedResult) { + const { renameFilename, renameLocation, edits } = result; + let mappedRenameLocation: protocol.Location | undefined; + if (renameFilename !== undefined && renameLocation !== undefined) { + 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; + } + } private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { Debug.assert(args.scope.type === "file"); diff --git a/src/services/_namespaces/ts.refactor.ts b/src/services/_namespaces/ts.refactor.ts index 19159057c6da6..ff3e6904abb6b 100644 --- a/src/services/_namespaces/ts.refactor.ts +++ b/src/services/_namespaces/ts.refactor.ts @@ -6,6 +6,7 @@ export * from "../refactors/convertImport"; export * from "../refactors/extractType"; export * from "../refactors/helpers"; export * from "../refactors/moveToNewFile"; +export * from "../refactors/moveToAnotherFile"; import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction"; export { addOrRemoveBracesToArrowFunction }; import * as convertArrowFunctionOrFunctionExpression from "./ts.refactor.convertArrowFunctionOrFunctionExpression"; diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 611c846c21c99..5d216f19aceb0 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -5,6 +5,7 @@ import { Refactor, RefactorContext, RefactorEditInfo, + SourceFile, } from "./_namespaces/ts"; import { refactorKindBeginsWith } from "./_namespaces/ts.refactor"; @@ -34,3 +35,9 @@ export function getEditsForRefactor(context: RefactorContext, refactorName: stri const refactor = refactors.get(refactorName); return refactor && refactor.getEditsForAction(context, actionName); } + +/** @internal */ +export function getEditsForMoveToFileRefactor(context: RefactorContext, newFile: SourceFile, refactorName: string, actionName: string): RefactorEditInfo | undefined { + const refactor = refactors.get(refactorName); + return refactor && refactor.getEditsForAction(context, actionName, newFile); +} diff --git a/src/services/refactors/moveToAnotherFile.ts b/src/services/refactors/moveToAnotherFile.ts new file mode 100644 index 0000000000000..66153dfa0f117 --- /dev/null +++ b/src/services/refactors/moveToAnotherFile.ts @@ -0,0 +1,982 @@ +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, + ExternalModuleReference, + factory, + find, + FindAllReferences, + findIndex, + firstDefined, + flatMap, + forEachEntry, + FunctionDeclaration, + getAssignmentDeclarationKind, + getBaseFileName, + getDecorators, + getDirectoryPath, + getLocaleSpecificMessage, + getModifiers, + getPropertySymbolFromBindingElement, + getQuotePreference, + getRangesWhere, + getRefactorContextSpan, + getSymbolId, + getUniqueName, + hasSyntacticModifier, + Identifier, + ImportDeclaration, + ImportEqualsDeclaration, + insertImports, + InterfaceDeclaration, + InternalSymbolName, + isBinaryExpression, + isBindingElement, + isDeclarationName, + isExpressionStatement, + isExternalModuleReference, + isIdentifier, + isImportDeclaration, + isImportEqualsDeclaration, + isNamedDeclaration, + isOmittedExpression, + isPrologueDirective, + isPropertyAccessExpression, + isRequireCall, + isSourceFile, + isStringLiteral, + isStringLiteralLike, + isVariableDeclaration, + isVariableDeclarationList, + isVariableStatement, + LanguageServiceHost, + length, + makeImportIfNecessary, + mapDefined, + ModifierFlags, + ModifierLike, + ModuleDeclaration, + NamedImportBindings, + Node, + NodeFlags, + nodeSeenTracker, + ObjectBindingElementWithoutPropertyName, + Program, + PropertyAccessExpression, + 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"; + +const refactorNameForAnotherFile = "Move to another file"; +const description = getLocaleSpecificMessage(Diagnostics.Move_to_another_file); + +const moveToAnotherFileAction = { + name: "Move to another file", //needs to change for it to have refactorName = "Move to another file" + description, + kind: "refactor.move.anotherFile", +}; +registerRefactor(refactorNameForAnotherFile, { + kinds: [moveToAnotherFileAction.kind], + getAvailableActions: function getRefactorActionsToMoveToAnotherFile(context): readonly ApplicableRefactorInfo[] { + const statements = getStatementsToMove(context); + if (context.preferences.allowTextChangesInNewFiles && statements) { + return [{ name: refactorNameForAnotherFile, description, actions: [moveToAnotherFileAction] }]; + } + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ name: refactorNameForAnotherFile, description, actions: + [{ ...moveToAnotherFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] + }]; + } + return emptyArray; + }, + getEditsForAction: function getRefactorEditsToMoveToAnotherFile(context, actionName, newFile): RefactorEditInfo | undefined { + Debug.assert(actionName === refactorNameForAnotherFile, "Wrong refactor invoked"); + const statements = Debug.checkDefined(getStatementsToMove(context)); + if (newFile) { + const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context.file, newFile, context.program, statements, t, context.host, context.preferences)); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } + return 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 doChangeToAnotherFile(oldFile: SourceFile, newFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { + const checker = program.getTypeChecker(); + const usage = getUsageInfo(oldFile, toMove.all, checker); + changes.addStatementsToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile.fileName, preferences)); +} + +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, /*checkArgumentIsStringLiteralLike*/ true)); + default: + return false; + } +} + +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; + } +} + +function getNewStatementsAndRemoveFromOldFile( + oldFile: SourceFile, _newFile: 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) { + deleteMovedStatements(oldFile, toMove.ranges, changes); + return [...prologueDirectives, ...toMove.all]; + } + + const useEsModuleSyntax = !!oldFile.externalModuleIndicator; + const quotePreference = getQuotePreference(oldFile, preferences); + const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); + if (importsFromNewFile) { + insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); + } + + deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); + deleteMovedStatements(oldFile, toMove.ranges, changes); + updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); + + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); + //insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); + const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); + //insert import statements in new file + + if (imports.length && body.length) { + return [ + ...prologueDirectives, + ...imports, + SyntaxKind.NewLineTrivia as const, + ...body + ]; + } + + return [ + ...prologueDirectives, + ...imports, + ...body, + ]; +} + +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, +): 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, factory.createStringLiteral(newModuleSpecifier), shouldMove); + if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); + + const ns = getNamespaceLikeImport(importNode); + if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleSpecifier, ns, importNode); + }); + } + } +} + +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, +): 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)); + } +} + +function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node { + const newNamespaceId = factory.createIdentifier(newNamespaceName); + const newModuleString = factory.createStringLiteral(newModuleSpecifier); + 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, /*checkArgumentIsStringLiteralLike*/ 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(factory.createStringLiteral(pathToNewFileWithCorrectExtension))) 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, + changes: textChanges.ChangeTracker, + checker: TypeChecker, + program: Program, + host: LanguageServiceHost, + useEsModuleSyntax: boolean, + quotePreference: QuotePreference, +): readonly AnyImportOrRequireStatement[] { + const copiedOldImports: AnyImportOrRequireStatement[] = []; + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); + } + + // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + let oldFileDefault: Identifier | undefined; + const oldFileNamedImports: string[] = []; + const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. + newFileImportsFromOldFile.forEach(symbol => { + if (!symbol.declarations) { + return; + } + for (const decl of symbol.declarations) { + if (!isTopLevelDeclaration(decl)) continue; + const name = nameOfTopLevelDeclaration(decl); + if (!name) continue; + + const top = getTopLevelDeclarationStatement(decl); + if (markSeenTop(top)) { + addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); + } + if (hasSyntacticModifier(decl, ModifierFlags.Default)) { + oldFileDefault = name; + } + else { + oldFileNamedImports.push(name.text); + } + } + }); + + 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; +} + +// 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, /*checkArgumentIsStringLiteralLike*/ 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(cb: (symbol: Symbol) => T | undefined): T | undefined; +} + +class SymbolSet implements ReadonlySymbolSet { + private map = new Map(); + 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(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(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(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(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))); +} + + diff --git a/src/services/services.ts b/src/services/services.ts index f5bba46bdbca7..e3395e7c3b9cb 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2946,6 +2946,20 @@ export function createLanguageService( return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName); } + function getEditsForMoveToFileRefactor( + fileName: string, + newFile: string, + formatOptions: FormatCodeSettings, + positionOrRange: number | TextRange, + refactorName: string, + actionName: string, + preferences: UserPreferences = emptyOptions, + ): RefactorEditInfo | undefined { + synchronizeHostData(); + const file = getValidSourceFile(fileName); + return refactor.getEditsForMoveToFileRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), getValidSourceFile(newFile), refactorName, actionName); + } + function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { // Go to Definition supports returning a zero-length span at position 0 for // non-existent files. We need to special-case the conversion of position 0 @@ -3040,6 +3054,7 @@ export function createLanguageService( updateIsDefinitionOfReferencedSymbols, getApplicableRefactors, getEditsForRefactor, + getEditsForMoveToFileRefactor, toLineColumnOffset, getSourceMapper: () => sourceMapper, clearSourceMapperCache: () => sourceMapper.clearCache(), diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 50621f641601d..ec202656f972f 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -483,6 +483,7 @@ export class ChangeTracker { private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; + private readonly changeExistingFile: { readonly oldFile: SourceFile, readonly newFile: SourceFile, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; public static fromContext(context: TextChangesContext): ChangeTracker { return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); @@ -1131,12 +1132,18 @@ export class ChangeTracker { for (const { oldFile, fileName, statements } of this.newFiles) { changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } + for (const { oldFile, newFile, statements } of this.changeExistingFile) { + changes.push(changesToText.existingFileChanges(oldFile, newFile.fileName, statements, this.newLineCharacter, this.formatContext)); + } return changes; } public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { this.newFiles.push({ oldFile, fileName, statements }); } + public addStatementsToNewFile(oldFile: SourceFile, newFile: SourceFile, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { + this.changeExistingFile.push({ oldFile, newFile, statements }); + } } function updateJSDocHost(parent: HasJSDoc): HasJSDoc { @@ -1246,6 +1253,11 @@ namespace changesToText { return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; } + export function existingFileChanges(oldFile: SourceFile, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { + const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, 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 { // 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); diff --git a/src/services/types.ts b/src/services/types.ts index d0ed926108ddd..e4517360fed91 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -634,6 +634,7 @@ export interface LanguageService { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -1753,7 +1754,7 @@ export interface Refactor { kinds?: string[]; /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined; + getEditsForAction(context: RefactorContext, actionName: string, newFile?: SourceFile): RefactorEditInfo | undefined; /** Compute (quickly) which actions are available here */ getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 53352c8573dc1..af11ebbc2dae8 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -452,7 +452,11 @@ declare namespace FourSlashInterface { readonly preferences?: UserPreferences; }): void; noMoveToNewFile(): void; - + moveToAnotherFile(options: { + readonly newFileContents: { readonly [fileName: string]: string }; + readonly newFile: string; + readonly preferences?: UserPreferences; + }): void; generateTypes(...options: GenerateTypesOptions[]): void; organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: UserPreferences): void; From ea8de385392d44d45946a023759d773cd97f0ed9 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 7 Mar 2023 15:38:41 -0800 Subject: [PATCH 03/42] new change --- src/services/textChanges.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index ec202656f972f..d1f46e05e448a 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1133,7 +1133,7 @@ export class ChangeTracker { changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } for (const { oldFile, newFile, statements } of this.changeExistingFile) { - changes.push(changesToText.existingFileChanges(oldFile, newFile.fileName, statements, this.newLineCharacter, this.formatContext)); + changes.push(changesToText.existingFileChanges(oldFile, newFile.fileName, statements, changes, this.newLineCharacter, this.formatContext)); } return changes; } @@ -1253,11 +1253,6 @@ namespace changesToText { return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; } - export function existingFileChanges(oldFile: SourceFile, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { - const text = newFileChangesWorker(oldFile, getScriptKindFromFileName(fileName), statements, 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 { // 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); @@ -1266,6 +1261,19 @@ namespace changesToText { return applyChanges(nonFormattedText, changes) + newLineCharacter; } + export function existingFileChanges(oldFile: SourceFile, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], changes: FileTextChanges[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { + const text = existingFileChangesWorker(oldFile, changes, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); + return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; + } + + export function existingFileChangesWorker(oldFile: SourceFile | undefined, _changes: FileTextChanges[], scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], 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 sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); + const changeFormatting = formatting.formatDocument(sourceFile, formatContext); + return applyChanges(nonFormattedText, changeFormatting) + newLineCharacter; + } + function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { if (change.kind === ChangeKind.Remove) { return ""; From 393b093fa3203d5a2cb67ee2506e12a8e3408f94 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 9 Mar 2023 16:11:25 -0800 Subject: [PATCH 04/42] test2 almost works --- src/harness/fourslashImpl.ts | 2 +- src/services/refactors/moveToAnotherFile.ts | 14 ++++--- tests/cases/fourslash/moveToAnotherFile2.ts | 5 ++- tests/cases/fourslash/moveToAnotherFile3.ts | 41 +++++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/cases/fourslash/moveToAnotherFile3.ts diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 312142c6166d2..b9a17a0ce5cd8 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3175,7 +3175,7 @@ export class TestState { } 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); + const newContent = ts.textChanges.applyChanges(oldText!, change.textChanges); this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent); } for (const newFileName in newFileContent) { diff --git a/src/services/refactors/moveToAnotherFile.ts b/src/services/refactors/moveToAnotherFile.ts index 66153dfa0f117..b95a05119eb01 100644 --- a/src/services/refactors/moveToAnotherFile.ts +++ b/src/services/refactors/moveToAnotherFile.ts @@ -145,8 +145,9 @@ registerRefactor(refactorNameForAnotherFile, { getEditsForAction: function getRefactorEditsToMoveToAnotherFile(context, actionName, newFile): RefactorEditInfo | undefined { Debug.assert(actionName === refactorNameForAnotherFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); + const newFileImportAdder = codefix.createImportAdder(context.file, context.program, context.preferences, context.host); if (newFile) { - const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context.file, newFile, context.program, statements, t, context.host, context.preferences)); + const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context.file, newFile, newFileImportAdder, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } return undefined; @@ -179,10 +180,10 @@ function getRangeToMove(context: RefactorContext): RangeToMove | undefined { }; } -function doChangeToAnotherFile(oldFile: SourceFile, newFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { +function doChangeToAnotherFile(oldFile: SourceFile, newFile: SourceFile, newFileImportAdder: codefix.ImportAdder, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - changes.addStatementsToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile.fileName, preferences)); + changes.addStatementsToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, newFileImportAdder, usage, changes, toMove, program, host, newFile.fileName, preferences)); } interface StatementRange { @@ -296,7 +297,7 @@ function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, _newFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, + oldFile: SourceFile, newFile: SourceFile,_newFileImportAdder: codefix.ImportAdder, 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); @@ -317,9 +318,10 @@ function getNewStatementsAndRemoveFromOldFile( updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); - //insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); + insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); + //newFileImportAdder.writeFixes(changes); //insert import statements in new file const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); - //insert import statements in new file + changes.insertNodeAfter(newFile, newFile.statements[newFile.statements.length-1], body[0]); //needs to be changed if (imports.length && body.length) { return [ diff --git a/tests/cases/fourslash/moveToAnotherFile2.ts b/tests/cases/fourslash/moveToAnotherFile2.ts index a08f2b02f43a3..d5f7eab804f90 100644 --- a/tests/cases/fourslash/moveToAnotherFile2.ts +++ b/tests/cases/fourslash/moveToAnotherFile2.ts @@ -3,6 +3,8 @@ //@Filename: /bar.ts ////import './blah'; ////import './blah2'; +////const a = 2; +////a; // @Filename: /a.ts ////// header comment @@ -29,7 +31,8 @@ a; y;`, import './blah'; import './blah2'; import { b } from './other'; - +const a = 2; +a; export const y: Date = p + b; `, }, diff --git a/tests/cases/fourslash/moveToAnotherFile3.ts b/tests/cases/fourslash/moveToAnotherFile3.ts new file mode 100644 index 0000000000000..8cb976d7ed77a --- /dev/null +++ b/tests/cases/fourslash/moveToAnotherFile3.ts @@ -0,0 +1,41 @@ +/// + +//@Filename: /bar.ts +////import './blah'; +////import './blah2'; +////const a = 2; +////a; + +// @Filename: /a.ts +////// header comment +//// +////import './foo'; +////import { a, b, alreadyUnused } from './other'; +////import { p } from './other2'; +////[|const y: Date = p + b;|] + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import './foo'; +import { a, alreadyUnused } from './other'; +`, + + "/bar.ts": +`import './blah'; +import './blah2'; +import { b } from './other'; +import { p } from './other2'; +const a = 2; +a; +const y: Date = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); From fe30722a1a7aae2df2afc82308c8d6a998bac938 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 13 Mar 2023 13:07:37 -0700 Subject: [PATCH 05/42] Part 2 works for test2 --- src/services/refactors/moveToAnotherFile.ts | 23 +++++++-- src/services/textChanges.ts | 53 ++++++++++++++------- tests/cases/fourslash/moveToAnotherFile.ts | 5 +- tests/cases/fourslash/moveToAnotherFile4.ts | 52 ++++++++++++++++++++ 4 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 tests/cases/fourslash/moveToAnotherFile4.ts diff --git a/src/services/refactors/moveToAnotherFile.ts b/src/services/refactors/moveToAnotherFile.ts index b95a05119eb01..21405ffce6db5 100644 --- a/src/services/refactors/moveToAnotherFile.ts +++ b/src/services/refactors/moveToAnotherFile.ts @@ -145,8 +145,8 @@ registerRefactor(refactorNameForAnotherFile, { getEditsForAction: function getRefactorEditsToMoveToAnotherFile(context, actionName, newFile): RefactorEditInfo | undefined { Debug.assert(actionName === refactorNameForAnotherFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - const newFileImportAdder = codefix.createImportAdder(context.file, context.program, context.preferences, context.host); if (newFile) { + const newFileImportAdder = codefix.createImportAdder(newFile, context.program, context.preferences, context.host); const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context.file, newFile, newFileImportAdder, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } @@ -297,7 +297,7 @@ function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: SourceFile,_newFileImportAdder: codefix.ImportAdder, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, + oldFile: SourceFile, newFile: SourceFile, _newFileImportAdder: codefix.ImportAdder, 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); @@ -318,10 +318,25 @@ function getNewStatementsAndRemoveFromOldFile( updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); - insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); + //insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); + //trial + // const changeTracker = textChanges.ChangeTracker.fromContext(context); + // const minInsertionPos = (isReadonlyArray(range.range) ? last(range.range) : range.range).end; + // const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope); + // if (nodeToInsertBefore) { + // changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, /*blankLineBetween*/ true); + // } + // else { + // changeTracker.insertNodeAtEndOfScope(context.file, scope, newFunction); + // } + + //changes.insertNodesAtTopOfFile(newFile, imports, /*blankLineBetween*/ false); //newFileImportAdder.writeFixes(changes); //insert import statements in new file + if (imports.length > 0) { + insertImports(changes, newFile, imports,/*blankLineBetween*/ true, preferences); + } const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); - changes.insertNodeAfter(newFile, newFile.statements[newFile.statements.length-1], body[0]); //needs to be changed + changes.insertNodesAfter(newFile, newFile.statements[newFile.statements.length-1], body); if (imports.length && body.length) { return [ diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index d1f46e05e448a..ebb9b7d79d8e3 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1128,13 +1128,17 @@ export class ChangeTracker { public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); - const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); + let changes; + if (this.changeExistingFile && this.changes) { + for (const { oldFile } of this.changeExistingFile) { + changes = changesToText.getTextChangesFromChangesForMoveToExistingFile(this.changes, oldFile, this.newLineCharacter, this.formatContext, validate); + return changes; + } + } + 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)); } - for (const { oldFile, newFile, statements } of this.changeExistingFile) { - changes.push(changesToText.existingFileChanges(oldFile, newFile.fileName, statements, changes, this.newLineCharacter, this.formatContext)); - } return changes; } @@ -1248,6 +1252,34 @@ namespace changesToText { }); } + export function getTextChangesFromChangesForMoveToExistingFile(changes: readonly Change[], oldFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { + return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => { + // order changes by start position + // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. + const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end)); + // verify that change intervals do not overlap, except possibly at end points. + for (let i = 0; i < normalized.length - 1; i++) { + Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () => + `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`); + } + + const sourceFile = changesInFile[0].sourceFile; + const textChanges = mapDefined(normalized, c => { + const span = createTextSpanFromRange(c.range); + const newText = computeNewText(c, oldFile, newLineCharacter, formatContext, validate); + + // Filter out redundant changes. + if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) { + return undefined; + } + + return createTextChange(span, newText); + }); + + return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined; + }); + } + 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); return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; @@ -1261,19 +1293,6 @@ namespace changesToText { return applyChanges(nonFormattedText, changes) + newLineCharacter; } - export function existingFileChanges(oldFile: SourceFile, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], changes: FileTextChanges[], newLineCharacter: string, formatContext: formatting.FormatContext): FileTextChanges { - const text = existingFileChangesWorker(oldFile, changes, getScriptKindFromFileName(fileName), statements, newLineCharacter, formatContext); - return { fileName, textChanges: [createTextChange(createTextSpan(0, 0), text)], isNewFile: true }; - } - - export function existingFileChangesWorker(oldFile: SourceFile | undefined, _changes: FileTextChanges[], scriptKind: ScriptKind, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], 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 sourceFile = createSourceFile("any file name", nonFormattedText, ScriptTarget.ESNext, /*setParentNodes*/ true, scriptKind); - const changeFormatting = formatting.formatDocument(sourceFile, formatContext); - return applyChanges(nonFormattedText, changeFormatting) + newLineCharacter; - } - function computeNewText(change: Change, sourceFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string { if (change.kind === ChangeKind.Remove) { return ""; diff --git a/tests/cases/fourslash/moveToAnotherFile.ts b/tests/cases/fourslash/moveToAnotherFile.ts index f6bd20fe7239b..41a44e1dc6542 100644 --- a/tests/cases/fourslash/moveToAnotherFile.ts +++ b/tests/cases/fourslash/moveToAnotherFile.ts @@ -1,7 +1,7 @@ /// //@Filename: /bar.ts -//// +//////header comment // @Filename: /a.ts ////// header comment @@ -24,7 +24,8 @@ export const p = 0; a; y;`, "/bar.ts": -`import { b } from './other'; +`//header comment +import { b } from './other'; import { p } from './a'; export const y: Date = p + b; diff --git a/tests/cases/fourslash/moveToAnotherFile4.ts b/tests/cases/fourslash/moveToAnotherFile4.ts new file mode 100644 index 0000000000000..99a863b3835cb --- /dev/null +++ b/tests/cases/fourslash/moveToAnotherFile4.ts @@ -0,0 +1,52 @@ +/// + +//@Filename: /bar.ts +////import './blah'; +////import './blah2'; +////const a = 2; +////a; + +// @Filename: /a.ts +////// header comment +//// +////import './foo'; +////import { a, b, alreadyUnused } from './other'; +////const p = 0; +////[|const x = 0; +////function f() {} +////class C {} +////enum E {} +////namespace N { export const x = 0; } +////type T = number; +////interface I {}|] + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import './foo'; +import { a, b, alreadyUnused } from './other'; +const p = 0; +`, + + "/bar.ts": +`import './blah'; +import './blah2'; +const a = 2; +a; +export const x = 0; +export function f() { } +export class C { } +export enum E { } +export namespace N { export const x = 0; } +export type T = number; +export interface I { } +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); From f54ece725e4c883d1122dc00245fb1bbc30c2829 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 21 Mar 2023 15:24:24 -0700 Subject: [PATCH 06/42] generating list of file with test cases --- src/harness/client.ts | 9 + src/harness/harnessLanguageService.ts | 3 + src/server/protocol.ts | 47 +++ src/server/session.ts | 42 ++- src/services/createNewFilename.ts | 336 ++++++++++++++++++ src/services/services.ts | 15 + src/services/types.ts | 1 + src/testRunner/tests.ts | 1 + .../getMoveToRefactoringFileSuggestions.ts | 143 ++++++++ 9 files changed, 592 insertions(+), 5 deletions(-) create mode 100644 src/services/createNewFilename.ts create mode 100644 src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 5c0eb6ee00b20..af41e4e61f0a3 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -52,6 +52,7 @@ import { Program, QuickInfo, RefactorEditInfo, + RefactorTriggerReason, ReferencedSymbol, ReferenceEntry, RenameInfo, @@ -802,6 +803,14 @@ export class SessionClient implements LanguageService { return response.body!; // TODO: GH#18217 } + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFilename: string | undefined; files: string[] | undefined; } { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args); + const response = this.processResponse(request); + return { newFilename: response.body?.newFileName, files:response.body?.files }!; // TODO: GH#18217 + } + getEditsForRefactor( fileName: string, _formatOptions: FormatCodeSettings, diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 055ab2fc50be8..fae76b8f4d68d 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -619,6 +619,9 @@ class LanguageServiceShimProxy implements ts.LanguageService { getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } + getMoveToRefactoringFileSuggestions(): { newFilename: string | undefined, files: string[] | undefined } { + 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."); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1ac6e1f495df0..4fec70d25313a 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -143,6 +143,8 @@ export const enum CommandTypes { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", + GetEditsForMoveToFileRefactor = "getEditsForMoveToFileRefactor", + GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", /** @internal */ GetEditsForRefactorFull = "getEditsForRefactor-full", @@ -599,6 +601,33 @@ 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 { //will have to change to FileLocationOrRangeRequestArgs + // Pass along the same arguments that we first passed to `GetApplicableRefactorsRequest` + command: CommandTypes.GetMoveToRefactoringFileSuggestions; + arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; +} +export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { + triggerReason?: RefactorTriggerReason; + 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 GetMoveToRefactoringFileSuggestionsResponse extends Response { + body?: { + /// Suggested name if a new file is created + newFileName: string; + + /// List of file paths that the selected text can be moved to + // TODO: should this be an object instead so that TS could customize how files are shown in the file picker UI? + files: string[]; + }; +} + /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ @@ -674,6 +703,24 @@ export interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; } +export interface GetEditsForMoveToFileRefactorRequest extends Request { + command: CommandTypes.GetEditsForMoveToFileRefactor; + arguments: GetEditsForMoveToFileRefactorRequestArgs; +} + +export interface GetEditsForMoveToFileRefactorResponse extends Response { + body?: RefactorEditInfo; // TODO: maybe use a new type +} + +export type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeRequestArgs & { + /* The 'name' property from the refactoring that offered this action */ + refactor: string; + /* The 'name' property from the refactoring action */ + action: string; + /* Target file path that the user selected (may also be new file) */ + filepath: string; +}; + export interface RefactorEditInfo { edits: FileCodeEdits[]; diff --git a/src/server/session.ts b/src/server/session.ts index 4dc8b08489ebc..8f0e5768b1109 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -877,7 +877,9 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.ApplyCodeActionCommand, protocol.CommandTypes.GetSupportedCodeFixes, protocol.CommandTypes.GetApplicableRefactors, + protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, protocol.CommandTypes.GetEditsForRefactor, + protocol.CommandTypes.GetEditsForMoveToFileRefactor, protocol.CommandTypes.GetEditsForRefactorFull, protocol.CommandTypes.OrganizeImports, protocol.CommandTypes.OrganizeImportsFull, @@ -2712,17 +2714,41 @@ export class Session implements EventSender { return result; } } -//@ts-ignore - private getEditsForMoveToFileRefactor(args: protocol.GetEditsForRefactorRequestArgs, newFile: string, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { + + private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFilename: string | undefined, files: string[] | undefined }{ + const { file } = this.getFileAndProject(args); + const allFiles: string[] = []; + let filename: string | undefined; + this.projectService.forEachEnabledProject(project => { + updateProjectIfDirty(project); + if (project.containsFile(file)) { + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + if (scriptInfo) { + const { newFilename, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind); + if (files) { + for (const file of files) { + if (!allFiles.includes(file)) { + allFiles.push(file); + } + } + } + filename = newFilename; + } + } + }); + return { newFilename: filename, files: allFiles }; + } + + private getEditsForMoveToFileForRefactor(args: protocol.GetEditsForMoveToFileRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const result = project.getLanguageService().getEditsForMoveToFileRefactor( file, - newFile, + args.filepath, this.getFormatOptions(file), this.extractPositionOrRange(args, scriptInfo), - "Move to another file", //args.refactor="Move to another file" - "Move to another file",//"args.action =Move to another file" + args.refactor, + args.action, this.getPreferences(file), ); @@ -3466,6 +3492,12 @@ export class Session implements EventSender { [protocol.CommandTypes.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => { return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true)); }, + [protocol.CommandTypes.GetEditsForMoveToFileRefactor]: (request: protocol.GetEditsForMoveToFileRefactorRequest) => { + return this.requiredResponse(this.getEditsForMoveToFileForRefactor(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)); }, diff --git a/src/services/createNewFilename.ts b/src/services/createNewFilename.ts new file mode 100644 index 0000000000000..a08f964862945 --- /dev/null +++ b/src/services/createNewFilename.ts @@ -0,0 +1,336 @@ +import { cast, contains, find, findIndex, firstDefined, getRangesWhere, some } from "../compiler/core"; +import { combinePaths, getDirectoryPath } from "../compiler/path"; +import { AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, ClassDeclaration, Declaration, EnumDeclaration, ExpressionStatement, FunctionDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, ModifierFlags, ModuleDeclaration, Node, Program, PropertyAccessExpression, SourceFile, Statement, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; +import { copyEntries, extensionFromPath, forEachEntry, getAssignmentDeclarationKind, hasSyntacticModifier, isDeclarationName, isPrologueDirective, isRequireCall } from "../compiler/utilities"; +import { createTextRangeFromSpan, Debug, getRefactorContextSpan, getSymbolId, isBinaryExpression, isBindingElement, isExpressionStatement, isIdentifier, isNamedDeclaration, isOmittedExpression, isSourceFile, isVariableDeclaration, rangeContainsRange, Symbol, symbolNameNoDefault } from "./_namespaces/ts"; +import { LanguageServiceHost, RefactorContext } from "./types"; + +interface ToMove { + readonly all: readonly Statement[]; + readonly ranges: readonly StatementRange[]; +} + +interface StatementRange { + readonly first: Statement; + readonly afterLast: Statement | undefined; +} + +interface ReadonlySymbolSet { + size(): number; + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +} + +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; +} +type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' +type NonVariableTopLevelDeclaration = + | FunctionDeclaration + | ClassDeclaration + | EnumDeclaration + | TypeAliasDeclaration + | InterfaceDeclaration + | ModuleDeclaration + | TopLevelExpressionStatement + | ImportEqualsDeclaration; +interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } +type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; + +/** @internal */ +export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { + const checker = program.getTypeChecker(); + const toMove = getStatementsToMove(context); + let usage; + if (toMove) { + 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; + return newFilename; + } + return ""; +} + +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 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, /*checkArgumentIsStringLiteralLike*/ true)); + default: + return false; + } +} + +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; + } +} + +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"; +} + +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); + } + }); +} + +function forEachTopLevelDeclaration(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 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, /*checkArgumentIsStringLiteralLike*/ true); +} + +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 forEachTopLevelDeclarationInBindingName(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 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; + } +} +interface ReadonlySymbolSet { + size(): number; + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +} + +class SymbolSet implements ReadonlySymbolSet { + private map = new Map(); + 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(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; + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index e3395e7c3b9cb..d0e3e30eb7f89 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -318,6 +318,7 @@ import { } from "./_namespaces/ts"; import * as NavigateTo from "./_namespaces/ts.NavigateTo"; import * as NavigationBar from "./_namespaces/ts.NavigationBar"; +import { createNewFilename } from "./createNewFilename"; /** The version of the language service API */ export const servicesVersion = "0.8"; @@ -2933,6 +2934,19 @@ export function createLanguageService( return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); } + function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): { newFilename: string | undefined, files: string[] | undefined } { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const program = getProgram(); + const files = program?.getSourceFiles().filter(sourceFile => !program?.isSourceFileFromExternalLibrary(sourceFile)).map(f => f.fileName); + //creating new filename + let newFilename; + if (program) { + newFilename = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions, triggerReason, kind), host); + } + return { newFilename, files }; + } + function getEditsForRefactor( fileName: string, formatOptions: FormatCodeSettings, @@ -3053,6 +3067,7 @@ export function createLanguageService( getAutoImportProvider, updateIsDefinitionOfReferencedSymbols, getApplicableRefactors, + getMoveToRefactoringFileSuggestions, getEditsForRefactor, getEditsForMoveToFileRefactor, toLineColumnOffset, diff --git a/src/services/types.ts b/src/services/types.ts index e4517360fed91..7297f79933c0b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -633,6 +633,7 @@ export interface LanguageService { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFilename: string | undefined, files: string[] | undefined }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; diff --git a/src/testRunner/tests.ts b/src/testRunner/tests.ts index c95ca207db5fb..5f977c40de8c3 100644 --- a/src/testRunner/tests.ts +++ b/src/testRunner/tests.ts @@ -192,3 +192,4 @@ import "./unittests/tsserver/versionCache"; import "./unittests/tsserver/watchEnvironment"; import "./unittests/debugDeprecation"; import "./unittests/tsserver/inconsistentErrorInEditor"; +import "./unittests/tsserver/getMoveToRefactoringFileSuggestions"; diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts new file mode 100644 index 0000000000000..b5194168916a8 --- /dev/null +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -0,0 +1,143 @@ +import * as ts from "../../_namespaces/ts"; +import { + createServerHost, + File, +} from "../virtualFileSystemWithWatch"; +import { + baselineTsserverLogs, + createLoggerWithInMemoryLogs, + createSession, + openFilesForSession, +} from "./helpers"; + +describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { + it("works for suggesting a list of files, excluding node_modules within a project and a new filename", () => { + 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/c/file3.ts", content: "" }; + const file4: File = { path: "/project/d/e/file4.ts", content: "" }; + const file5: File = { + path: "/project/a/file5.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 files = [file1, file2, file3, file4, file5, nodeModulesFile1, nodeModulesFile2, tsconfig]; + const host = createServerHost(files); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + session.executeCommandSeq({ + 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 and a new filename", session); + }); + it("works for suggesting a list of files, for a file belonging to multiple projects", () => { + const file1: File = { + path: "/blah/file1.ts", + content: `export const value1 = 0;` + }; + const file2: File = { path: "/blah/file2.ts", content: "" }; + const configFile1: File = { + path: "/blah/tsconfig.json", + content: `{ "files": ["./file1.ts", "./file2.ts"] }`, + }; + const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; + const file4: File = { path: "/blah-tests/file4.ts", content: "" }; + const configFile2: File = { + path: "/blah-tests/tsconfig.json", + content: `{ "files": ["./file3.ts", "./file4.ts"] }`, + }; + const files = [file1, file2, file3, file4, configFile1, configFile2]; + const host = createServerHost(files); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 14 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); + }); + it("works for suggesting a list of files, for an inferred project .ts file", () => { + const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; + const file2: File = { + path: "/file2.ts", + content: `interface ka { + name: string; + } + ` + }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; + + const host = createServerHost([file1, file2, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file2], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file2.path, line: 1, offset: 14 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); + }); + it("works for suggesting a list of files, for an inferred project .js file", () => { + const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; + const file2: File = { + path: "/file2.js", + content: `const y = p + b;` + }; + const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; + + const host = createServerHost([file1, file2, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file2], session); + + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file2.path, line: 1, offset: 1, endLine: 1, endOffset: 16 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); + }); +// Check +// 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 ./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({ +// command: ts.server.protocol.CommandTypes.GetEditsForMoveToFileRefactor, +// arguments: { +// file: aTs.path, +// startLine: 1, +// startOffset: 1, +// endLine: 2, +// endOffset: aTs.content.length, +// refactor: "Move to another file", +// action: "Move to another file", +// filepath: "/Foo/b.ts", +// } +// }); +// baselineTsserverLogs("refactors", "handles canonicalization of tsconfig path", session); +// }); +}); \ No newline at end of file From 9bfd52d6817a94f25b490b2da8ba0ad082703c83 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sat, 25 Mar 2023 19:20:05 -0700 Subject: [PATCH 07/42] Test cases working as expected --- src/harness/client.ts | 1 - src/harness/fourslashImpl.ts | 4 +- src/services/refactorProvider.ts | 3 +- src/services/refactors/moveToAnotherFile.ts | 122 ++++++----- src/services/services.ts | 16 +- src/services/textChanges.ts | 4 +- src/services/types.ts | 2 +- .../getMoveToRefactoringFileSuggestions.ts | 206 +++++++++++------- tests/cases/fourslash/moveToFile1.ts | 43 ++++ tests/cases/fourslash/moveToFile2.ts | 30 +++ tests/cases/fourslash/moveToFile3.ts | 39 ++++ tests/cases/fourslash/moveToFile4.ts | 39 ++++ tests/cases/fourslash/moveToFile5.ts | 44 ++++ .../{moveToAnotherFile3.ts => moveToFile6.ts} | 19 +- .../{moveToAnotherFile2.ts => moveToFile7.ts} | 0 ...oAnotherFile.ts => moveToFile8_newFile.ts} | 6 +- ...4.ts => moveToFile9_multipleStatements.ts} | 20 +- 17 files changed, 435 insertions(+), 163 deletions(-) create mode 100644 tests/cases/fourslash/moveToFile1.ts create mode 100644 tests/cases/fourslash/moveToFile2.ts create mode 100644 tests/cases/fourslash/moveToFile3.ts create mode 100644 tests/cases/fourslash/moveToFile4.ts create mode 100644 tests/cases/fourslash/moveToFile5.ts rename tests/cases/fourslash/{moveToAnotherFile3.ts => moveToFile6.ts} (70%) rename tests/cases/fourslash/{moveToAnotherFile2.ts => moveToFile7.ts} (100%) rename tests/cases/fourslash/{moveToAnotherFile.ts => moveToFile8_newFile.ts} (82%) rename tests/cases/fourslash/{moveToAnotherFile4.ts => moveToFile9_multipleStatements.ts} (72%) diff --git a/src/harness/client.ts b/src/harness/client.ts index af41e4e61f0a3..faaa4aa8cbce7 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -52,7 +52,6 @@ import { Program, QuickInfo, RefactorEditInfo, - RefactorTriggerReason, ReferencedSymbol, ReferenceEntry, RenameInfo, diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index b9a17a0ce5cd8..091108b727695 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3174,8 +3174,8 @@ 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 = ts.textChanges.applyChanges(oldText!, change.textChanges); + //ts.Debug.assert(!!change.isNewFile === (oldText === undefined)); //TODO: fix newText? + const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges); this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent); } for (const newFileName in newFileContent) { diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 5d216f19aceb0..15b4ceec1c9bd 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -5,7 +5,6 @@ import { Refactor, RefactorContext, RefactorEditInfo, - SourceFile, } from "./_namespaces/ts"; import { refactorKindBeginsWith } from "./_namespaces/ts.refactor"; @@ -37,7 +36,7 @@ export function getEditsForRefactor(context: RefactorContext, refactorName: stri } /** @internal */ -export function getEditsForMoveToFileRefactor(context: RefactorContext, newFile: SourceFile, refactorName: string, actionName: string): RefactorEditInfo | undefined { +export function getEditsForMoveToFileRefactor(context: RefactorContext, newFile: string, refactorName: string, actionName: string): RefactorEditInfo | undefined { const refactor = refactors.get(refactorName); return refactor && refactor.getEditsForAction(context, actionName, newFile); } diff --git a/src/services/refactors/moveToAnotherFile.ts b/src/services/refactors/moveToAnotherFile.ts index 21405ffce6db5..2cf9464c2510a 100644 --- a/src/services/refactors/moveToAnotherFile.ts +++ b/src/services/refactors/moveToAnotherFile.ts @@ -13,7 +13,7 @@ import { canHaveSymbol, cast, ClassDeclaration, codefix, - //combinePaths, + combinePaths, concatenate, contains, copyEntries, @@ -39,6 +39,7 @@ import { FunctionDeclaration, getAssignmentDeclarationKind, getBaseFileName, + GetCanonicalFileName, getDecorators, getDirectoryPath, getLocaleSpecificMessage, @@ -47,15 +48,18 @@ import { getQuotePreference, getRangesWhere, getRefactorContextSpan, + getRelativePathFromFile, getSymbolId, getUniqueName, hasSyntacticModifier, + hostGetCanonicalFileName, Identifier, ImportDeclaration, ImportEqualsDeclaration, insertImports, InterfaceDeclaration, InternalSymbolName, + isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, @@ -65,9 +69,11 @@ import { isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, + isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, + isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, @@ -76,6 +82,7 @@ import { isVariableDeclarationList, isVariableStatement, LanguageServiceHost, + last, length, makeImportIfNecessary, mapDefined, @@ -86,9 +93,11 @@ import { Node, NodeFlags, nodeSeenTracker, + normalizePath, ObjectBindingElementWithoutPropertyName, Program, PropertyAccessExpression, + PropertyAssignment, QuotePreference, rangeContainsRange, RefactorContext, @@ -146,8 +155,7 @@ registerRefactor(refactorNameForAnotherFile, { Debug.assert(actionName === refactorNameForAnotherFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); if (newFile) { - const newFileImportAdder = codefix.createImportAdder(newFile, context.program, context.preferences, context.host); - const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context.file, newFile, newFileImportAdder, context.program, statements, t, context.host, context.preferences)); + const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } return undefined; @@ -180,10 +188,37 @@ function getRangeToMove(context: RefactorContext): RangeToMove | undefined { }; } -function doChangeToAnotherFile(oldFile: SourceFile, newFile: SourceFile, newFileImportAdder: codefix.ImportAdder, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { +function doChangeToAnotherFile(context: RefactorContext, oldFile: SourceFile, newFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - changes.addStatementsToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, newFileImportAdder, usage, changes, toMove, program, host, newFile.fileName, preferences)); + //creating a new file + if (!host.fileExists(newFile)) { + const newFilename = newFile; + changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFilename, preferences, /*newFileExists*/ false)); + addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); + } + else { + const sourceFile = program.getSourceFile(newFile); + if (sourceFile) { + const newFileImportAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); + changes.addStatementsToNewFile(oldFile, program.getSourceFile(newFile), getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder)); + } + } +} + +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); + } } interface StatementRange { @@ -245,17 +280,18 @@ function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker 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); + for (const decl of symbol.declarations) {//not needed + if (isInImport(decl)) { + oldImportsNeededByNewFile.add(symbol); + } + else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { + newFileImportsFromOldFile.add(symbol); + } } - } }); } @@ -297,7 +333,7 @@ function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: SourceFile, _newFileImportAdder: codefix.ImportAdder, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, + oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, newFileImportAdder?: codefix.ImportAdder ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); @@ -317,27 +353,20 @@ function getNewStatementsAndRemoveFromOldFile( deleteMovedStatements(oldFile, toMove.ranges, changes); updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference); - //insertImports(changes, newFile, imports, /*blankLineBetween*/ true, preferences); - //trial - // const changeTracker = textChanges.ChangeTracker.fromContext(context); - // const minInsertionPos = (isReadonlyArray(range.range) ? last(range.range) : range.range).end; - // const nodeToInsertBefore = getNodeToInsertFunctionBefore(minInsertionPos, scope); - // if (nodeToInsertBefore) { - // changeTracker.insertNodeBefore(context.file, nodeToInsertBefore, newFunction, /*blankLineBetween*/ true); - // } - // else { - // changeTracker.insertNodeAtEndOfScope(context.file, scope, newFunction); - // } - - //changes.insertNodesAtTopOfFile(newFile, imports, /*blankLineBetween*/ false); - //newFileImportAdder.writeFixes(changes); //insert import statements in new file - if (imports.length > 0) { - insertImports(changes, newFile, imports,/*blankLineBetween*/ true, preferences); - } + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, newFileImportAdder); const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); - changes.insertNodesAfter(newFile, newFile.statements[newFile.statements.length-1], body); - + if (newFileExists) { + const newFileSourceFile = program.getSourceFile(newFile); + if (newFileSourceFile && newFileSourceFile.statements.length > 0) { + changes.insertNodesAfter(newFileSourceFile, newFileSourceFile.statements[newFileSourceFile.statements.length - 1], body); + } + if (imports.length > 0 && newFileSourceFile) { + insertImports(changes, newFileSourceFile, imports, /*blankLineBetween*/ true, preferences); + } + } + if (newFileImportAdder) { + newFileImportAdder.writeFixes(changes); + } if (imports.length && body.length) { return [ ...prologueDirectives, @@ -648,6 +677,7 @@ function getNewFileImportsAndAddExportInOldFile( host: LanguageServiceHost, useEsModuleSyntax: boolean, quotePreference: QuotePreference, + newFileImportAdder?: codefix.ImportAdder, ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; for (const oldStatement of oldFile.statements) { @@ -656,7 +686,7 @@ function getNewFileImportsAndAddExportInOldFile( }); } - // Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. let oldFileDefault: Identifier | undefined; const oldFileNamedImports: string[] = []; const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. @@ -673,11 +703,16 @@ function getNewFileImportsAndAddExportInOldFile( if (markSeenTop(top)) { addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); } - if (hasSyntacticModifier(decl, ModifierFlags.Default)) { - oldFileDefault = name; + if (newFileImportAdder && symbol.parent !== undefined) { //exported + newFileImportAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); } else { - oldFileNamedImports.push(name.text); + if (hasSyntacticModifier(decl, ModifierFlags.Default)) { + oldFileDefault = name; + } + else { + oldFileNamedImports.push(name.text); + } } } }); @@ -686,19 +721,6 @@ function getNewFileImportsAndAddExportInOldFile( 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; diff --git a/src/services/services.ts b/src/services/services.ts index d0e3e30eb7f89..50b82297a93fc 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2938,7 +2938,19 @@ export function createLanguageService( synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const program = getProgram(); - const files = program?.getSourceFiles().filter(sourceFile => !program?.isSourceFileFromExternalLibrary(sourceFile)).map(f => f.fileName); + const allFiles = program?.getSourceFiles().filter(sourceFile => !program?.isSourceFileFromExternalLibrary(sourceFile)).map(f => f.fileName); + const extension = ts.extensionFromPath(fileName); + const files: string[] = []; + if (allFiles) { + for (const file of allFiles) { + if (ts.extensionIsTS(extension) && ts.hasTSFileExtension(file)) { + files.push(file); + } + else if (ts.fileExtensionIs(file, extension)){ + files.push(file); + } + } + } //creating new filename let newFilename; if (program) { @@ -2971,7 +2983,7 @@ export function createLanguageService( ): RefactorEditInfo | undefined { synchronizeHostData(); const file = getValidSourceFile(fileName); - return refactor.getEditsForMoveToFileRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), getValidSourceFile(newFile), refactorName, actionName); + return refactor.getEditsForMoveToFileRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), newFile, refactorName, actionName); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index ebb9b7d79d8e3..ab1741ddce237 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -483,7 +483,7 @@ export class ChangeTracker { private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; - private readonly changeExistingFile: { readonly oldFile: SourceFile, readonly newFile: SourceFile, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; + private readonly changeExistingFile: { readonly oldFile: SourceFile, readonly newFile: SourceFile | undefined, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; public static fromContext(context: TextChangesContext): ChangeTracker { return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); @@ -1145,7 +1145,7 @@ export class ChangeTracker { public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { this.newFiles.push({ oldFile, fileName, statements }); } - public addStatementsToNewFile(oldFile: SourceFile, newFile: SourceFile, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { + public addStatementsToNewFile(oldFile: SourceFile, newFile: SourceFile | undefined, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { this.changeExistingFile.push({ oldFile, newFile, statements }); } } diff --git a/src/services/types.ts b/src/services/types.ts index 7297f79933c0b..953ca9ba4ddc4 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1755,7 +1755,7 @@ export interface Refactor { kinds?: string[]; /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string, newFile?: SourceFile): RefactorEditInfo | undefined; + getEditsForAction(context: RefactorContext, actionName: string, newFile?: string): RefactorEditInfo | undefined; /** Compute (quickly) which actions are available here */ getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[]; diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts index b5194168916a8..4164451db0a67 100644 --- a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -11,108 +11,154 @@ import { } from "./helpers"; describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { - it("works for suggesting a list of files, excluding node_modules within a project and a new filename", () => { +// it("works for suggesting a list of files, excluding node_modules within a project and a new filename", () => { +// 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/c/file3.ts", content: "" }; +// const file4: File = { path: "/project/d/e/file4.ts", content: "" }; +// const file5: File = { +// path: "/project/a/file5.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 files = [file1, file2, file3, file4, file5, nodeModulesFile1, nodeModulesFile2, tsconfig]; +// const host = createServerHost(files); +// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); +// openFilesForSession([file1], session); +// session.executeCommandSeq({ +// 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 and a new filename", session); +// }); +// it("works for suggesting a list of files, for a file belonging to multiple projects", () => { +// const file1: File = { +// path: "/blah/file1.ts", +// content: `export const value1 = 0;` +// }; +// const file2: File = { path: "/blah/file2.ts", content: "" }; +// const configFile1: File = { +// path: "/blah/tsconfig.json", +// content: `{ "files": ["./file1.ts", "./file2.ts"] }`, +// }; +// const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; +// const file4: File = { path: "/blah-tests/file4.ts", content: "" }; +// const configFile2: File = { +// path: "/blah-tests/tsconfig.json", +// content: `{ "files": ["./file3.ts", "./file4.ts"] }`, +// }; +// const files = [file1, file2, file3, file4, configFile1, configFile2]; +// const host = createServerHost(files); +// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); +// openFilesForSession([file1], session); +// session.executeCommandSeq({ +// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, +// arguments: { file: file1.path, line: 1, offset: 14 } +// }); +// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); +// }); +// it("works for suggesting a list of files, for an inferred project .ts file", () => { +// const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; +// const file2: File = { +// path: "/file2.ts", +// content: `interface ka { +// name: string; +// } +// ` +// }; +// const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; + +// const host = createServerHost([file1, file2, tsconfig]); +// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); +// openFilesForSession([file2], session); + +// session.executeCommandSeq({ +// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, +// arguments: { file: file2.path, line: 1, offset: 14 } +// }); +// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); +// }); +// it("works for suggesting a list of files, for an inferred project .js file", () => { +// const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; +// const file2: File = { +// path: "/file2.js", +// content: `const y = p + b;` +// }; +// const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; + +// const host = createServerHost([file1, file2, tsconfig]); +// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); +// openFilesForSession([file2], session); + +// session.executeCommandSeq({ +// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, +// arguments: { file: file2.path, line: 1, offset: 1, endLine: 1, endOffset: 16 } +// }); +// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); +// }); + it("works with different .ts extensions", () => { const file1: File = { - path: "/project/a/file1.ts", + path: "/file1.ts", content: `interface ka { name: string; } ` }; - const file2: File = { path: "/project/b/file2.ts", content: "" }; - const file3: File = { path: "/project/c/file3.ts", content: "" }; - const file4: File = { path: "/project/d/e/file4.ts", content: "" }; - const file5: File = { - path: "/project/a/file5.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 files = [file1, file2, file3, file4, file5, nodeModulesFile1, nodeModulesFile2, tsconfig]; - const host = createServerHost(files); + 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 tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file2.tsx", "./file3.mts", "./file4.cts", "./file5.js", "./file6.d.ts"] }) }; + + const host = createServerHost([file1, file2, file3, file4, file5, file6, tsconfig]); const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); openFilesForSession([file1], session); + session.executeCommandSeq({ 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 and a new filename", session); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different extensions", session); }); - it("works for suggesting a list of files, for a file belonging to multiple projects", () => { + it("works with different extensions", () => { const file1: File = { - path: "/blah/file1.ts", - content: `export const value1 = 0;` - }; - const file2: File = { path: "/blah/file2.ts", content: "" }; - const configFile1: File = { - path: "/blah/tsconfig.json", - content: `{ "files": ["./file1.ts", "./file2.ts"] }`, - }; - const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; - const file4: File = { path: "/blah-tests/file4.ts", content: "" }; - const configFile2: File = { - path: "/blah-tests/tsconfig.json", - content: `{ "files": ["./file3.ts", "./file4.ts"] }`, - }; - const files = [file1, file2, file3, file4, configFile1, configFile2]; - const host = createServerHost(files); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file1], session); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file1.path, line: 1, offset: 14 } - }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); - }); - it("works for suggesting a list of files, for an inferred project .ts file", () => { - const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; - const file2: File = { - path: "/file2.ts", - content: `interface ka { - name: string; - } - ` - }; - const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; - - const host = createServerHost([file1, file2, tsconfig]); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file2], session); - - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file2.path, line: 1, offset: 14 } - }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); - }); - it("works for suggesting a list of files, for an inferred project .js file", () => { - const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; - const file2: File = { - path: "/file2.js", + path: "/file1.js", content: `const y = p + b;` }; - const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; + 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, tsconfig]); + const host = createServerHost([file1, file2, file3, file4, file5, tsconfig]); const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file2], session); + openFilesForSession([file1], session); session.executeCommandSeq({ command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file2.path, line: 1, offset: 1, endLine: 1, endOffset: 16 } + arguments: { file: file1.path, line: 1, offset: 1, endLine: 1, endOffset:16 } }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different extensions", session); }); // Check // it("handles moving statement to an existing file", () => { diff --git a/tests/cases/fourslash/moveToFile1.ts b/tests/cases/fourslash/moveToFile1.ts new file mode 100644 index 0000000000000..b838952972254 --- /dev/null +++ b/tests/cases/fourslash/moveToFile1.ts @@ -0,0 +1,43 @@ + +/// + +// @Filename: /bar.ts +////const q = 0; + +// @Filename: /a.ts +////// header comment +//// +////import { } from './other'; +////import type { } from './other'; +////export const p = 0; +////export const b = 1; +////[|const y = p + b;|] + +// @Filename: /other.ts +////export const t = 2; + + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import { } from './other'; +import type { } from './other'; +export const p = 0; +export const b = 1; +`, + + "/bar.ts": +`import { p, b } from './a'; + +const q = 0; +const y = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile2.ts new file mode 100644 index 0000000000000..8153fe6bc02f9 --- /dev/null +++ b/tests/cases/fourslash/moveToFile2.ts @@ -0,0 +1,30 @@ + +/// + + +// @Filename: /bar.ts +//// const q = 0; + +// @Filename: /a.ts +////import { type A } from "./other"; +////[|function f(a: A) {}|] + +// @Filename: /other.ts +////export interface A { +//// x: number; +////} + + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts":"", + "/bar.ts": +` import { type A } from "./other"; + + const q = 0; + function f(a: A) { } +`, + }, + newFile: "/bar.ts", + preferences: {} +}); diff --git a/tests/cases/fourslash/moveToFile3.ts b/tests/cases/fourslash/moveToFile3.ts new file mode 100644 index 0000000000000..dd4d95ec83384 --- /dev/null +++ b/tests/cases/fourslash/moveToFile3.ts @@ -0,0 +1,39 @@ + +/// + +// @Filename: /bar.ts +//// import type {} from './a'; +//// +////const q = 0; + +// @Filename: /a.ts +////// header comment +//// +////export const p = 0; +////export const b = 1; +////[|const y = p + b;|] + + + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +export const p = 0; +export const b = 1; +`, + + "/bar.ts": +` import { b, p } from './a'; + +const q = 0; +const y = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToFile4.ts b/tests/cases/fourslash/moveToFile4.ts new file mode 100644 index 0000000000000..1517755855a83 --- /dev/null +++ b/tests/cases/fourslash/moveToFile4.ts @@ -0,0 +1,39 @@ + +/// + +// @Filename: /bar.ts +//// import { b } from './a'; +//// +////const q = 0; + +// @Filename: /a.ts +////// header comment +//// +////export const p = 0; +////export const b = 1; +////[|const y = p + b;|] + + + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +export const p = 0; +export const b = 1; +`, + + "/bar.ts": +` import { b, p } from './a'; + +const q = 0; +const y = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToFile5.ts b/tests/cases/fourslash/moveToFile5.ts new file mode 100644 index 0000000000000..76e0f83a0fa36 --- /dev/null +++ b/tests/cases/fourslash/moveToFile5.ts @@ -0,0 +1,44 @@ +/// + +//@Filename: /bar.ts +////import './someFile'; +//// +////const q = 20; + +// @Filename: /a.ts +////// header comment +//// +////import './foo'; +////import { a, b } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] + +// @Filename: /other.ts +////export const b = 2; +////export const a = 1; + +verify.moveToAnotherFile({ + newFileContents: { + "/a.ts": +`// header comment + +import './foo'; +import { a } from './other'; +export const p = 0; +`, + + "/bar.ts": +`import { p } from './a'; +import { b } from './other'; +import './someFile'; + +const q = 20; +const y: Date = p + b; +`, + }, + newFile: "/bar.ts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToAnotherFile3.ts b/tests/cases/fourslash/moveToFile6.ts similarity index 70% rename from tests/cases/fourslash/moveToAnotherFile3.ts rename to tests/cases/fourslash/moveToFile6.ts index 8cb976d7ed77a..0d29e40d6661f 100644 --- a/tests/cases/fourslash/moveToAnotherFile3.ts +++ b/tests/cases/fourslash/moveToFile6.ts @@ -1,17 +1,16 @@ /// //@Filename: /bar.ts -////import './blah'; -////import './blah2'; -////const a = 2; -////a; +////import './someFile'; +//// +////const q = 20; // @Filename: /a.ts ////// header comment //// ////import './foo'; ////import { a, b, alreadyUnused } from './other'; -////import { p } from './other2'; +////const p = 0; ////[|const y: Date = p + b;|] verify.moveToAnotherFile({ @@ -21,15 +20,15 @@ verify.moveToAnotherFile({ import './foo'; import { a, alreadyUnused } from './other'; +export const p = 0; `, "/bar.ts": -`import './blah'; -import './blah2'; +`import { p } from './a'; import { b } from './other'; -import { p } from './other2'; -const a = 2; -a; +import './someFile'; + +const q = 20; const y: Date = p + b; `, }, diff --git a/tests/cases/fourslash/moveToAnotherFile2.ts b/tests/cases/fourslash/moveToFile7.ts similarity index 100% rename from tests/cases/fourslash/moveToAnotherFile2.ts rename to tests/cases/fourslash/moveToFile7.ts diff --git a/tests/cases/fourslash/moveToAnotherFile.ts b/tests/cases/fourslash/moveToFile8_newFile.ts similarity index 82% rename from tests/cases/fourslash/moveToAnotherFile.ts rename to tests/cases/fourslash/moveToFile8_newFile.ts index 41a44e1dc6542..86d2b958b36e7 100644 --- a/tests/cases/fourslash/moveToAnotherFile.ts +++ b/tests/cases/fourslash/moveToFile8_newFile.ts @@ -1,8 +1,5 @@ /// -//@Filename: /bar.ts -//////header comment - // @Filename: /a.ts ////// header comment //// @@ -24,8 +21,7 @@ export const p = 0; a; y;`, "/bar.ts": -`//header comment -import { b } from './other'; +`import { b } from './other'; import { p } from './a'; export const y: Date = p + b; diff --git a/tests/cases/fourslash/moveToAnotherFile4.ts b/tests/cases/fourslash/moveToFile9_multipleStatements.ts similarity index 72% rename from tests/cases/fourslash/moveToAnotherFile4.ts rename to tests/cases/fourslash/moveToFile9_multipleStatements.ts index 99a863b3835cb..439d7ecfe857e 100644 --- a/tests/cases/fourslash/moveToAnotherFile4.ts +++ b/tests/cases/fourslash/moveToFile9_multipleStatements.ts @@ -13,35 +13,39 @@ ////import { a, b, alreadyUnused } from './other'; ////const p = 0; ////[|const x = 0; +////const t = 2; ////function f() {} ////class C {} ////enum E {} ////namespace N { export const x = 0; } ////type T = number; ////interface I {}|] +////t; verify.moveToAnotherFile({ newFileContents: { "/a.ts": `// header comment +import { t } from './bar'; import './foo'; import { a, b, alreadyUnused } from './other'; const p = 0; -`, +t;`, "/bar.ts": `import './blah'; import './blah2'; const a = 2; a; -export const x = 0; -export function f() { } -export class C { } -export enum E { } -export namespace N { export const x = 0; } -export type T = number; -export interface I { } +const x = 0; + export const t = 2; + function f() { } + class C { } + enum E { } + namespace N { export const x = 0; } + type T = number; + interface I { } `, }, newFile: "/bar.ts", From 58f2c9d69868c41c7358b939f7dad30aef72f320 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sun, 26 Mar 2023 15:31:17 -0700 Subject: [PATCH 08/42] cleanup of tests and renaming to "move to file" --- src/compiler/diagnosticMessages.json | 2 +- src/harness/fourslashImpl.ts | 6 +- src/harness/fourslashInterfaceImpl.ts | 6 +- src/server/protocol.ts | 3 +- src/services/_namespaces/ts.refactor.ts | 2 +- .../{moveToAnotherFile.ts => moveToFile.ts} | 55 +++-- .../getMoveToRefactoringFileSuggestions.ts | 227 ++++++++---------- .../unittests/tsserver/refactors.ts | 26 ++ ...excluding-node_modules-within-a-project.js | 203 ++++++++++++++++ ...r-a-file-belonging-to-multiple-projects.js | 148 ++++++++++++ ...-for-an-inferred-project-for-a-.js-file.js | 177 ++++++++++++++ ...-for-an-inferred-project-for-a-.ts-file.js | 163 +++++++++++++ .../works-with-different-.ts-extensions.js | 193 +++++++++++++++ .../works-with-different-extensions.js | 174 ++++++++++++++ tests/cases/fourslash/fourslash.ts | 2 +- tests/cases/fourslash/moveToFile1.ts | 2 +- tests/cases/fourslash/moveToFile2.ts | 2 +- tests/cases/fourslash/moveToFile3.ts | 2 +- tests/cases/fourslash/moveToFile4.ts | 2 +- tests/cases/fourslash/moveToFile5.ts | 2 +- tests/cases/fourslash/moveToFile6.ts | 2 +- tests/cases/fourslash/moveToFile7.ts | 2 +- tests/cases/fourslash/moveToFile8_newFile.ts | 2 +- .../moveToFile9_multipleStatements.ts | 2 +- 24 files changed, 1229 insertions(+), 176 deletions(-) rename src/services/refactors/{moveToAnotherFile.ts => moveToFile.ts} (93%) create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3091e94e600fa..241ceb65a3bbf 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7552,7 +7552,7 @@ "category": "Message", "code": 95175 }, - "Move to another file": { + "Move to file": { "category": "Message", "code": 95176 }, diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 091108b727695..5546300fb392b 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3873,13 +3873,13 @@ export class TestState { this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits); } - public moveToAnotherFile(options: FourSlashInterface.MoveToAnotherFileOptions): void { + 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 }), r => r.name === "Move to another file")!; + const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }), r => r.name === "Move to file")!; assert(refactor.actions.length === 1); const action = ts.first(refactor.actions); - assert(action.name === "Move to another file" && action.description === "Move to another file"); + assert(action.name === "Move to file" && action.description === "Move to file"); const editInfo = this.languageService.getEditsForMoveToFileRefactor(range.fileName, options.newFile, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions)!; this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index ba7517389470a..f2823124ea6a8 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -638,8 +638,8 @@ export class Verify extends VerifyNegatable { this.state.moveToNewFile(options); } - public moveToAnotherFile(options: MoveToAnotherFileOptions): void { - this.state.moveToAnotherFile(options); + public moveToFile(options: MoveToFileOptions): void { + this.state.moveToFile(options); } public noMoveToNewFile(): void { @@ -1924,7 +1924,7 @@ export interface MoveToNewFileOptions { readonly preferences?: ts.UserPreferences; } -export interface MoveToAnotherFileOptions { +export interface MoveToFileOptions { readonly newFileContents: { readonly [fileName: string]: string }; readonly newFile: string; readonly preferences?: ts.UserPreferences; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 4fec70d25313a..f4b8a754a326d 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -604,8 +604,7 @@ export interface GetApplicableRefactorsResponse extends Response { /** * Request refactorings at a given position or selection area to move to an existing file. */ -export interface GetMoveToRefactoringFileSuggestionsRequest extends Request { //will have to change to FileLocationOrRangeRequestArgs - // Pass along the same arguments that we first passed to `GetApplicableRefactorsRequest` +export interface GetMoveToRefactoringFileSuggestionsRequest extends Request { command: CommandTypes.GetMoveToRefactoringFileSuggestions; arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; } diff --git a/src/services/_namespaces/ts.refactor.ts b/src/services/_namespaces/ts.refactor.ts index ff3e6904abb6b..2edf81a5b8caa 100644 --- a/src/services/_namespaces/ts.refactor.ts +++ b/src/services/_namespaces/ts.refactor.ts @@ -6,7 +6,7 @@ export * from "../refactors/convertImport"; export * from "../refactors/extractType"; export * from "../refactors/helpers"; export * from "../refactors/moveToNewFile"; -export * from "../refactors/moveToAnotherFile"; +export * from "../refactors/moveToFile"; import * as addOrRemoveBracesToArrowFunction from "./ts.refactor.addOrRemoveBracesToArrowFunction"; export { addOrRemoveBracesToArrowFunction }; import * as convertArrowFunctionOrFunctionExpression from "./ts.refactor.convertArrowFunctionOrFunctionExpression"; diff --git a/src/services/refactors/moveToAnotherFile.ts b/src/services/refactors/moveToFile.ts similarity index 93% rename from src/services/refactors/moveToAnotherFile.ts rename to src/services/refactors/moveToFile.ts index 2cf9464c2510a..27a670c3288b7 100644 --- a/src/services/refactors/moveToAnotherFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -129,33 +129,33 @@ import { } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -const refactorNameForAnotherFile = "Move to another file"; -const description = getLocaleSpecificMessage(Diagnostics.Move_to_another_file); +const refactorNameForMoveToFile = "Move to file"; +const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); -const moveToAnotherFileAction = { - name: "Move to another file", //needs to change for it to have refactorName = "Move to another file" +const moveToFileAction = { + name: "Move to file", description, - kind: "refactor.move.anotherFile", + kind: "refactor.move.file", }; -registerRefactor(refactorNameForAnotherFile, { - kinds: [moveToAnotherFileAction.kind], - getAvailableActions: function getRefactorActionsToMoveToAnotherFile(context): readonly ApplicableRefactorInfo[] { +registerRefactor(refactorNameForMoveToFile, { + kinds: [moveToFileAction.kind], + getAvailableActions: function getRefactorActionsToMoveToFile(context): readonly ApplicableRefactorInfo[] { const statements = getStatementsToMove(context); if (context.preferences.allowTextChangesInNewFiles && statements) { - return [{ name: refactorNameForAnotherFile, description, actions: [moveToAnotherFileAction] }]; + return [{ name: refactorNameForMoveToFile, description, actions: [moveToFileAction] }]; } if (context.preferences.provideRefactorNotApplicableReason) { - return [{ name: refactorNameForAnotherFile, description, actions: - [{ ...moveToAnotherFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] + return [{ name: refactorNameForMoveToFile, description, actions: + [{ ...moveToFileAction, notApplicableReason: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_statement_or_statements) }] }]; } return emptyArray; }, - getEditsForAction: function getRefactorEditsToMoveToAnotherFile(context, actionName, newFile): RefactorEditInfo | undefined { - Debug.assert(actionName === refactorNameForAnotherFile, "Wrong refactor invoked"); + getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, newFile): RefactorEditInfo | undefined { + Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); if (newFile) { - const edits = textChanges.ChangeTracker.with(context, t => doChangeToAnotherFile(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } return undefined; @@ -188,14 +188,13 @@ function getRangeToMove(context: RefactorContext): RangeToMove | undefined { }; } -function doChangeToAnotherFile(context: RefactorContext, oldFile: SourceFile, newFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { +function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); //creating a new file if (!host.fileExists(newFile)) { - const newFilename = newFile; - changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFilename, preferences, /*newFileExists*/ false)); - addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); + changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); + addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); } else { const sourceFile = program.getSourceFile(newFile); @@ -333,7 +332,7 @@ function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, newFileImportAdder?: codefix.ImportAdder + oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); @@ -353,19 +352,19 @@ function getNewStatementsAndRemoveFromOldFile( deleteMovedStatements(oldFile, toMove.ranges, changes); updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, newFileImportAdder); + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); if (newFileExists) { - const newFileSourceFile = program.getSourceFile(newFile); - if (newFileSourceFile && newFileSourceFile.statements.length > 0) { - changes.insertNodesAfter(newFileSourceFile, newFileSourceFile.statements[newFileSourceFile.statements.length - 1], body); + const sourceFile = program.getSourceFile(newFile); + if (sourceFile && sourceFile.statements.length > 0) { + changes.insertNodesAfter(sourceFile, sourceFile.statements[sourceFile.statements.length - 1], body); } - if (imports.length > 0 && newFileSourceFile) { - insertImports(changes, newFileSourceFile, imports, /*blankLineBetween*/ true, preferences); + if (imports.length > 0 && sourceFile) { + insertImports(changes, sourceFile, imports, /*blankLineBetween*/ true, preferences); } } - if (newFileImportAdder) { - newFileImportAdder.writeFixes(changes); + if (importAdder) { + importAdder.writeFixes(changes); } if (imports.length && body.length) { return [ @@ -703,7 +702,7 @@ function getNewFileImportsAndAddExportInOldFile( if (markSeenTop(top)) { addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); } - if (newFileImportAdder && symbol.parent !== undefined) { //exported + if (newFileImportAdder && symbol.parent !== undefined) { newFileImportAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); } else { diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts index 4164451db0a67..ef188d0bfe809 100644 --- a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -11,109 +11,106 @@ import { } from "./helpers"; describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { -// it("works for suggesting a list of files, excluding node_modules within a project and a new filename", () => { -// 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/c/file3.ts", content: "" }; -// const file4: File = { path: "/project/d/e/file4.ts", content: "" }; -// const file5: File = { -// path: "/project/a/file5.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 files = [file1, file2, file3, file4, file5, nodeModulesFile1, nodeModulesFile2, tsconfig]; -// const host = createServerHost(files); -// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); -// openFilesForSession([file1], session); -// session.executeCommandSeq({ -// 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 and a new filename", session); -// }); -// it("works for suggesting a list of files, for a file belonging to multiple projects", () => { -// const file1: File = { -// path: "/blah/file1.ts", -// content: `export const value1 = 0;` -// }; -// const file2: File = { path: "/blah/file2.ts", content: "" }; -// const configFile1: File = { -// path: "/blah/tsconfig.json", -// content: `{ "files": ["./file1.ts", "./file2.ts"] }`, -// }; -// const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; -// const file4: File = { path: "/blah-tests/file4.ts", content: "" }; -// const configFile2: File = { -// path: "/blah-tests/tsconfig.json", -// content: `{ "files": ["./file3.ts", "./file4.ts"] }`, -// }; -// const files = [file1, file2, file3, file4, configFile1, configFile2]; -// const host = createServerHost(files); -// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); -// openFilesForSession([file1], session); -// session.executeCommandSeq({ -// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, -// arguments: { file: file1.path, line: 1, offset: 14 } -// }); -// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); -// }); -// it("works for suggesting a list of files, for an inferred project .ts file", () => { -// const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; -// const file2: File = { -// path: "/file2.ts", -// content: `interface ka { -// name: string; -// } -// ` -// }; -// const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; + 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({ + 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("works for suggesting a list of files, for a file belonging to multiple projects", () => { + const file1: File = { + path: "/blah/file1.ts", + content: `class CC { }` + }; + const file2: File = { path: "/blah/file2.ts", content: "" }; + const configFile1: File = { + path: "/blah/tsconfig.json", + content: `{ "files": ["./file1.ts", "./file2.ts"] }`, + }; + const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; + const file4: File = { path: "/blah-tests/file4.ts", content: "" }; + const configFile2: File = { + path: "/blah-tests/tsconfig.json", + content: `{ "files": ["./file3.ts", "./file4.ts"] }`, + }; + const host = createServerHost([file1, file2, file3, file4, configFile1, configFile2]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file1], session); + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); + }); + it("works for suggesting a list of files, for an inferred project for a .ts file", () => { + const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; + const file2: File = { + path: "/file2.ts", + content: `interface ka { + name: string; + } + ` + }; + const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; -// const host = createServerHost([file1, file2, tsconfig]); -// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); -// openFilesForSession([file2], session); + const host = createServerHost([file1, file2, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file2], session); -// session.executeCommandSeq({ -// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, -// arguments: { file: file2.path, line: 1, offset: 14 } -// }); -// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); -// }); -// it("works for suggesting a list of files, for an inferred project .js file", () => { -// const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; -// const file2: File = { -// path: "/file2.js", -// content: `const y = p + b;` -// }; -// const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file2.path, line: 1, offset: 11 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project for a .ts file", session); + }); + it("works for suggesting a list of files, for an inferred project for a .js file", () => { + const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; + const file2: File = { + path: "/file2.js", + content: `class C {}` + }; + const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; -// const host = createServerHost([file1, file2, tsconfig]); -// const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); -// openFilesForSession([file2], session); + const host = createServerHost([file1, file2, tsconfig]); + const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); + openFilesForSession([file2], session); -// session.executeCommandSeq({ -// command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, -// arguments: { file: file2.path, line: 1, offset: 1, endLine: 1, endOffset: 16 } -// }); -// baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project .ts file", session); -// }); + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file2.path, line: 1, offset: 7 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project for a .js file", session); + }); it("works with different .ts extensions", () => { const file1: File = { path: "/file1.ts", @@ -137,12 +134,12 @@ describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, arguments: { file: file1.path, line: 1, offset: 11 } }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different extensions", session); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different .ts extensions", session); }); it("works with different extensions", () => { const file1: File = { path: "/file1.js", - content: `const y = p + b;` + content: `class C {}` }; const file2: File = { path: "/file2.js", content: "" }; const file3: File = { path: "/file3.mts", content: "" }; @@ -156,34 +153,8 @@ describe("unittests:: tsserver:: getMoveToRefactoringFileSuggestions", () => { session.executeCommandSeq({ command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file1.path, line: 1, offset: 1, endLine: 1, endOffset:16 } + arguments: { file: file1.path, line: 1, offset: 7 } }); baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different extensions", session); }); -// Check -// 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 ./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({ -// command: ts.server.protocol.CommandTypes.GetEditsForMoveToFileRefactor, -// arguments: { -// file: aTs.path, -// startLine: 1, -// startOffset: 1, -// endLine: 2, -// endOffset: aTs.content.length, -// refactor: "Move to another file", -// action: "Move to another file", -// filepath: "/Foo/b.ts", -// } -// }); -// baselineTsserverLogs("refactors", "handles canonicalization of tsconfig path", session); -// }); }); \ No newline at end of file diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index c8885712174ff..49d6f28805915 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -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({ + command: ts.server.protocol.CommandTypes.GetEditsForMoveToFileRefactor, + arguments: { + file: aTs.path, + startLine: 1, + startOffset: 1, + endLine: 2, + endOffset: aTs.content.length, + refactor: "Move to file", + action: "Move to file", + filepath: "/Foo/b.ts", + } + }); + baselineTsserverLogs("refactors", "handles moving statement to an existing file", session); + }); }); diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js new file mode 100644 index 0000000000000..2d5074fc8379a --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -0,0 +1,203 @@ +Info 0 [00:00:36.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:37.000] request: + { + "command": "open", + "arguments": { + "file": "/project/a/file1.ts" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/project/a/file1.ts] +interface ka { + name: string; + } + + +//// [/project/b/file2.ts] + + +//// [/project/d/e/file3.ts] + + +//// [/project/a/file4.ts] +import { value } from "../node_modules/@types/node/someFile.d.ts"; +import { value1 } from "../node_modules/.cache/someFile.d.ts"; + +//// [/project/node_modules/@types/node/someFile.d.ts] +export const value = 0; + +//// [/project/node_modules/.cache/someFile.d.ts] +export const value1 = 0; + +//// [/project/tsconfig.json] +{} + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:38.000] Search path: /project/a +Info 3 [00:00:39.000] For info: /project/a/file1.ts :: Config file name: /project/tsconfig.json +Info 4 [00:00:40.000] Creating configuration project /project/tsconfig.json +Info 5 [00:00:41.000] FileWatcher:: Added:: WatchInfo: /project/tsconfig.json 2000 undefined Project: /project/tsconfig.json WatchType: Config file +Info 6 [00:00:42.000] Config: /project/tsconfig.json : { + "rootNames": [ + "/project/a/file1.ts", + "/project/a/file4.ts", + "/project/b/file2.ts", + "/project/d/e/file3.ts" + ], + "options": { + "configFilePath": "/project/tsconfig.json" + } +} +Info 7 [00:00:43.000] DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info 8 [00:00:44.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info 9 [00:00:45.000] FileWatcher:: Added:: WatchInfo: /project/a/file4.ts 500 undefined WatchType: Closed Script info +Info 10 [00:00:46.000] FileWatcher:: Added:: WatchInfo: /project/b/file2.ts 500 undefined WatchType: Closed Script info +Info 11 [00:00:47.000] FileWatcher:: Added:: WatchInfo: /project/d/e/file3.ts 500 undefined WatchType: Closed Script info +Info 12 [00:00:48.000] Starting updateGraphWorker: Project: /project/tsconfig.json +Info 13 [00:00:49.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations +Info 14 [00:00:50.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations +Info 15 [00:00:51.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info 16 [00:00:52.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info 17 [00:00:53.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /project/tsconfig.json WatchType: Missing file +Info 18 [00:00:54.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots +Info 19 [00:00:55.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots +Info 20 [00:00:56.000] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 21 [00:00:57.000] Project '/project/tsconfig.json' (Configured) +Info 22 [00:00:58.000] Files (6) + /project/a/file1.ts + /project/node_modules/@types/node/someFile.d.ts + /project/node_modules/.cache/someFile.d.ts + /project/a/file4.ts + /project/b/file2.ts + /project/d/e/file3.ts + + + a/file1.ts + Matched by default include pattern '**/*' + node_modules/@types/node/someFile.d.ts + Imported via "../node_modules/@types/node/someFile.d.ts" from file 'a/file4.ts' + node_modules/.cache/someFile.d.ts + Imported via "../node_modules/.cache/someFile.d.ts" from file 'a/file4.ts' + a/file4.ts + Matched by default include pattern '**/*' + b/file2.ts + Matched by default include pattern '**/*' + d/e/file3.ts + Matched by default include pattern '**/*' + +Info 23 [00:00:59.000] ----------------------------------------------- +Info 24 [00:01:00.000] Project '/project/tsconfig.json' (Configured) +Info 24 [00:01:01.000] Files (6) + +Info 24 [00:01:02.000] ----------------------------------------------- +Info 24 [00:01:03.000] Open files: +Info 24 [00:01:04.000] FileName: /project/a/file1.ts ProjectRootPath: undefined +Info 24 [00:01:05.000] Projects: /project/tsconfig.json +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/project/tsconfig.json: + {} +/project/a/file4.ts: + {} +/project/b/file2.ts: + {} +/project/d/e/file3.ts: + {} + +FsWatchesRecursive:: +/project: + {} +/project/node_modules: + {} +/project/node_modules/@types: + {} + +Info 24 [00:01:06.000] response: + { + "responseRequired": false + } +Info 25 [00:01:07.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/project/a/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/project/tsconfig.json: + {} +/project/a/file4.ts: + {} +/project/b/file2.ts: + {} +/project/d/e/file3.ts: + {} + +FsWatchesRecursive:: +/project: + {} +/project/node_modules: + {} +/project/node_modules/@types: + {} + +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/project/tsconfig.json: + {} +/project/a/file4.ts: + {} +/project/b/file2.ts: + {} +/project/d/e/file3.ts: + {} + +FsWatchesRecursive:: +/project: + {} +/project/node_modules: + {} +/project/node_modules/@types: + {} + +Info 26 [00:01:08.000] response: + { + "response": { + "newFilename": "/project/a/ka.ts", + "files": [ + "/project/a/file1.ts", + "/project/a/file4.ts", + "/project/b/file2.ts", + "/project/d/e/file3.ts" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js new file mode 100644 index 0000000000000..f925ebb3a5506 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js @@ -0,0 +1,148 @@ +Info 0 [00:00:19.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:20.000] request: + { + "command": "open", + "arguments": { + "file": "/blah/file1.ts" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/blah/file1.ts] +class CC { } + +//// [/blah/file2.ts] + + +//// [/blah-tests/file3.ts] +import { value1 } from "../blah/file1.ts"; + +//// [/blah-tests/file4.ts] + + +//// [/blah/tsconfig.json] +{ "files": ["./file1.ts", "./file2.ts"] } + +//// [/blah-tests/tsconfig.json] +{ "files": ["./file3.ts", "./file4.ts"] } + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:21.000] Search path: /blah +Info 3 [00:00:22.000] For info: /blah/file1.ts :: Config file name: /blah/tsconfig.json +Info 4 [00:00:23.000] Creating configuration project /blah/tsconfig.json +Info 5 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /blah/tsconfig.json 2000 undefined Project: /blah/tsconfig.json WatchType: Config file +Info 6 [00:00:25.000] Config: /blah/tsconfig.json : { + "rootNames": [ + "/blah/file1.ts", + "/blah/file2.ts" + ], + "options": { + "configFilePath": "/blah/tsconfig.json" + } +} +Info 7 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /blah/file2.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:27.000] Starting updateGraphWorker: Project: /blah/tsconfig.json +Info 9 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /blah/tsconfig.json WatchType: Missing file +Info 10 [00:00:29.000] DirectoryWatcher:: Added:: WatchInfo: /blah/node_modules/@types 1 undefined Project: /blah/tsconfig.json WatchType: Type roots +Info 11 [00:00:30.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /blah/node_modules/@types 1 undefined Project: /blah/tsconfig.json WatchType: Type roots +Info 12 [00:00:31.000] Finishing updateGraphWorker: Project: /blah/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 13 [00:00:32.000] Project '/blah/tsconfig.json' (Configured) +Info 14 [00:00:33.000] Files (2) + /blah/file1.ts + /blah/file2.ts + + + file1.ts + Part of 'files' list in tsconfig.json + file2.ts + Part of 'files' list in tsconfig.json + +Info 15 [00:00:34.000] ----------------------------------------------- +Info 16 [00:00:35.000] Project '/blah/tsconfig.json' (Configured) +Info 16 [00:00:36.000] Files (2) + +Info 16 [00:00:37.000] ----------------------------------------------- +Info 16 [00:00:38.000] Open files: +Info 16 [00:00:39.000] FileName: /blah/file1.ts ProjectRootPath: undefined +Info 16 [00:00:40.000] Projects: /blah/tsconfig.json +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} +/blah/node_modules/@types: + {"pollingInterval":500} + +FsWatches:: +/blah/tsconfig.json: + {} +/blah/file2.ts: + {} + +FsWatchesRecursive:: + +Info 16 [00:00:41.000] response: + { + "responseRequired": false + } +Info 17 [00:00:42.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/blah/file1.ts", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} +/blah/node_modules/@types: + {"pollingInterval":500} + +FsWatches:: +/blah/tsconfig.json: + {} +/blah/file2.ts: + {} + +FsWatchesRecursive:: + +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} +/blah/node_modules/@types: + {"pollingInterval":500} + +FsWatches:: +/blah/tsconfig.json: + {} +/blah/file2.ts: + {} + +FsWatchesRecursive:: + +Info 18 [00:00:43.000] response: + { + "response": { + "newFilename": "/blah/CC.ts", + "files": [ + "/blah/file1.ts", + "/blah/file2.ts" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js new file mode 100644 index 0000000000000..f619e6226cba9 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js @@ -0,0 +1,177 @@ +Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.js" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/file1.js] +import {} from "./file.js"; + +//// [/file2.js] +class C {} + +//// [/jsconfig.json] +{"files":["./file1.js","./file.js"]} + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:11.000] Search path: / +Info 3 [00:00:12.000] For info: /file2.js :: Config file name: /jsconfig.json +Info 4 [00:00:13.000] Creating configuration project /jsconfig.json +Info 5 [00:00:14.000] FileWatcher:: Added:: WatchInfo: /jsconfig.json 2000 undefined Project: /jsconfig.json WatchType: Config file +Info 6 [00:00:15.000] Config: /jsconfig.json : { + "rootNames": [ + "/file1.js", + "/file.js" + ], + "options": { + "allowJs": true, + "maxNodeModuleJsDepth": 2, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "noEmit": true, + "configFilePath": "/jsconfig.json" + } +} +Info 7 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /file1.js 500 undefined WatchType: Closed Script info +Info 8 [00:00:17.000] Starting updateGraphWorker: Project: /jsconfig.json +Info 9 [00:00:18.000] DirectoryWatcher:: Added:: WatchInfo: /file.js 1 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 10 [00:00:19.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /file.js 1 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 11 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 12 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 13 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file.js 500 undefined Project: /jsconfig.json WatchType: Missing file +Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /jsconfig.json WatchType: Missing file +Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /jsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 16 [00:00:25.000] Project '/jsconfig.json' (Configured) +Info 17 [00:00:26.000] Files (1) + /file1.js + + + file1.js + Part of 'files' list in tsconfig.json + +Info 18 [00:00:27.000] ----------------------------------------------- +Info 19 [00:00:28.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* +Info 20 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file +Info 21 [00:00:30.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 22 [00:00:31.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 23 [00:00:32.000] Files (1) + /file2.js + + + file2.js + Root file specified for compilation + +Info 24 [00:00:33.000] ----------------------------------------------- +Info 25 [00:00:34.000] Project '/jsconfig.json' (Configured) +Info 25 [00:00:35.000] Files (1) + +Info 25 [00:00:36.000] ----------------------------------------------- +Info 25 [00:00:37.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 25 [00:00:38.000] Files (1) + +Info 25 [00:00:39.000] ----------------------------------------------- +Info 25 [00:00:40.000] Open files: +Info 25 [00:00:41.000] FileName: /file2.js ProjectRootPath: undefined +Info 25 [00:00:42.000] Projects: /dev/null/inferredProject1* +After request + +PolledWatches:: +/file.js: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} +/bower_components: + {"pollingInterval":500} +/node_modules: + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: + {} +/file1.js: + {} +/: + {} + +FsWatchesRecursive:: + +Info 25 [00:00:43.000] response: + { + "responseRequired": false + } +Info 26 [00:00:44.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file2.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/file.js: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} +/bower_components: + {"pollingInterval":500} +/node_modules: + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: + {} +/file1.js: + {} +/: + {} + +FsWatchesRecursive:: + +After request + +PolledWatches:: +/file.js: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} +/bower_components: + {"pollingInterval":500} +/node_modules: + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: + {} +/file1.js: + {} +/: + {} + +FsWatchesRecursive:: + +Info 27 [00:00:45.000] response: + { + "response": { + "newFilename": "/C.js", + "files": [ + "/file2.js" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js new file mode 100644 index 0000000000000..280b473102d1e --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js @@ -0,0 +1,163 @@ +Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.ts" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/file1.ts] +import {} from "./file.ts"; + +//// [/file2.ts] +interface ka { + name: string; + } + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file.ts"]} + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:11.000] Search path: / +Info 3 [00:00:12.000] For info: /file2.ts :: Config file name: /tsconfig.json +Info 4 [00:00:13.000] Creating configuration project /tsconfig.json +Info 5 [00:00:14.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:15.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /file1.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:17.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 9 [00:00:18.000] DirectoryWatcher:: Added:: WatchInfo: /file.ts 1 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 10 [00:00:19.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /file.ts 1 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 11 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 12 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: 0 undefined Project: /tsconfig.json WatchType: Failed Lookup Locations +Info 13 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 16 [00:00:25.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:26.000] Files (1) + /file1.ts + + + file1.ts + Part of 'files' list in tsconfig.json + +Info 18 [00:00:27.000] ----------------------------------------------- +Info 19 [00:00:28.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* +Info 20 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file +Info 21 [00:00:30.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 22 [00:00:31.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 23 [00:00:32.000] Files (1) + /file2.ts + + + file2.ts + Root file specified for compilation + +Info 24 [00:00:33.000] ----------------------------------------------- +Info 25 [00:00:34.000] Project '/tsconfig.json' (Configured) +Info 25 [00:00:35.000] Files (1) + +Info 25 [00:00:36.000] ----------------------------------------------- +Info 25 [00:00:37.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 25 [00:00:38.000] Files (1) + +Info 25 [00:00:39.000] ----------------------------------------------- +Info 25 [00:00:40.000] Open files: +Info 25 [00:00:41.000] FileName: /file2.ts ProjectRootPath: undefined +Info 25 [00:00:42.000] Projects: /dev/null/inferredProject1* +After request + +PolledWatches:: +/file.ts: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file1.ts: + {} +/: + {} + +FsWatchesRecursive:: + +Info 25 [00:00:43.000] response: + { + "responseRequired": false + } +Info 26 [00:00:44.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file2.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/file.ts: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file1.ts: + {} +/: + {} + +FsWatchesRecursive:: + +After request + +PolledWatches:: +/file.ts: + {"pollingInterval":500} +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file1.ts: + {} +/: + {} + +FsWatchesRecursive:: + +Info 27 [00:00:45.000] response: + { + "response": { + "newFilename": "/ka.ts", + "files": [ + "/file2.ts" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js new file mode 100644 index 0000000000000..b4c9125449ee0 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js @@ -0,0 +1,193 @@ +Info 0 [00:00:17.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:18.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.ts" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/file1.ts] +interface ka { + name: string; + } + + +//// [/file2.tsx] + + +//// [/file3.mts] + + +//// [/file4.cts] + + +//// [/file5.js] + + +//// [/file6.d.ts] + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts"]} + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:19.000] Search path: / +Info 3 [00:00:20.000] For info: /file1.ts :: Config file name: /tsconfig.json +Info 4 [00:00:21.000] Creating configuration project /tsconfig.json +Info 5 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:23.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file5.js", + "/file6.d.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info +Info 8 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info +Info 10 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info +Info 12 [00:00:29.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 13 [00:00:30.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 14 [00:00:31.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 15 [00:00:32.000] Project '/tsconfig.json' (Configured) +Info 16 [00:00:33.000] Files (6) + /file1.ts + /file2.tsx + /file3.mts + /file4.cts + /file5.js + /file6.d.ts + + + file1.ts + Part of 'files' list in tsconfig.json + file2.tsx + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.cts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + file6.d.ts + Part of 'files' list in tsconfig.json + +Info 17 [00:00:34.000] ----------------------------------------------- +Info 18 [00:00:35.000] Project '/tsconfig.json' (Configured) +Info 18 [00:00:36.000] Files (6) + +Info 18 [00:00:37.000] ----------------------------------------------- +Info 18 [00:00:38.000] Open files: +Info 18 [00:00:39.000] FileName: /file1.ts ProjectRootPath: undefined +Info 18 [00:00:40.000] Projects: /tsconfig.json +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.tsx: + {} +/file3.mts: + {} +/file4.cts: + {} +/file5.js: + {} +/file6.d.ts: + {} + +FsWatchesRecursive:: + +Info 18 [00:00:41.000] response: + { + "responseRequired": false + } +Info 19 [00:00:42.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.tsx: + {} +/file3.mts: + {} +/file4.cts: + {} +/file5.js: + {} +/file6.d.ts: + {} + +FsWatchesRecursive:: + +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.tsx: + {} +/file3.mts: + {} +/file4.cts: + {} +/file5.js: + {} +/file6.d.ts: + {} + +FsWatchesRecursive:: + +Info 20 [00:00:43.000] response: + { + "response": { + "newFilename": "/ka.ts", + "files": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file6.d.ts" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js new file mode 100644 index 0000000000000..e2d2a27ad69a8 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js @@ -0,0 +1,174 @@ +Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info 1 [00:00:16.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.js" + }, + "seq": 1, + "type": "request" + } +Before request +//// [/file1.js] +class C {} + +//// [/file2.js] + + +//// [/file3.mts] + + +//// [/file4.ts] + + +//// [/file5.js] + + +//// [/tsconfig.json] +{"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} + + +PolledWatches:: + +FsWatches:: + +FsWatchesRecursive:: + +Info 2 [00:00:17.000] Search path: / +Info 3 [00:00:18.000] For info: /file1.js :: Config file name: /tsconfig.json +Info 4 [00:00:19.000] Creating configuration project /tsconfig.json +Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:21.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.js", + "/file2.js", + "/file3.mts", + "/file4.ts", + "/file5.js" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info +Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info +Info 10 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:26.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 12 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 13 [00:00:28.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 14 [00:00:29.000] Project '/tsconfig.json' (Configured) +Info 15 [00:00:30.000] Files (5) + /file1.js + /file2.js + /file3.mts + /file4.ts + /file5.js + + + file1.js + Part of 'files' list in tsconfig.json + file2.js + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.ts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + +Info 16 [00:00:31.000] ----------------------------------------------- +Info 17 [00:00:32.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:33.000] Files (5) + +Info 17 [00:00:34.000] ----------------------------------------------- +Info 17 [00:00:35.000] Open files: +Info 17 [00:00:36.000] FileName: /file1.js ProjectRootPath: undefined +Info 17 [00:00:37.000] Projects: /tsconfig.json +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.js: + {} +/file3.mts: + {} +/file4.ts: + {} +/file5.js: + {} + +FsWatchesRecursive:: + +Info 17 [00:00:38.000] response: + { + "responseRequired": false + } +Info 18 [00:00:39.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Before request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.js: + {} +/file3.mts: + {} +/file4.ts: + {} +/file5.js: + {} + +FsWatchesRecursive:: + +After request + +PolledWatches:: +/a/lib/lib.d.ts: + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: + {} +/file2.js: + {} +/file3.mts: + {} +/file4.ts: + {} +/file5.js: + {} + +FsWatchesRecursive:: + +Info 19 [00:00:40.000] response: + { + "response": { + "newFilename": "/C.js", + "files": [ + "/file1.js", + "/file2.js", + "/file5.js" + ] + }, + "responseRequired": true + } \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index af11ebbc2dae8..5ff814e84c6c6 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -452,7 +452,7 @@ declare namespace FourSlashInterface { readonly preferences?: UserPreferences; }): void; noMoveToNewFile(): void; - moveToAnotherFile(options: { + moveToFile(options: { readonly newFileContents: { readonly [fileName: string]: string }; readonly newFile: string; readonly preferences?: UserPreferences; diff --git a/tests/cases/fourslash/moveToFile1.ts b/tests/cases/fourslash/moveToFile1.ts index b838952972254..7043ef3aad3b4 100644 --- a/tests/cases/fourslash/moveToFile1.ts +++ b/tests/cases/fourslash/moveToFile1.ts @@ -17,7 +17,7 @@ ////export const t = 2; -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile2.ts index 8153fe6bc02f9..26167fde76ce8 100644 --- a/tests/cases/fourslash/moveToFile2.ts +++ b/tests/cases/fourslash/moveToFile2.ts @@ -15,7 +15,7 @@ ////} -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts":"", "/bar.ts": diff --git a/tests/cases/fourslash/moveToFile3.ts b/tests/cases/fourslash/moveToFile3.ts index dd4d95ec83384..75fc870ed66d2 100644 --- a/tests/cases/fourslash/moveToFile3.ts +++ b/tests/cases/fourslash/moveToFile3.ts @@ -15,7 +15,7 @@ -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile4.ts b/tests/cases/fourslash/moveToFile4.ts index 1517755855a83..933c1b02b680b 100644 --- a/tests/cases/fourslash/moveToFile4.ts +++ b/tests/cases/fourslash/moveToFile4.ts @@ -15,7 +15,7 @@ -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile5.ts b/tests/cases/fourslash/moveToFile5.ts index 76e0f83a0fa36..d9b2b884ce721 100644 --- a/tests/cases/fourslash/moveToFile5.ts +++ b/tests/cases/fourslash/moveToFile5.ts @@ -17,7 +17,7 @@ ////export const b = 2; ////export const a = 1; -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile6.ts b/tests/cases/fourslash/moveToFile6.ts index 0d29e40d6661f..d8279abef4c40 100644 --- a/tests/cases/fourslash/moveToFile6.ts +++ b/tests/cases/fourslash/moveToFile6.ts @@ -13,7 +13,7 @@ ////const p = 0; ////[|const y: Date = p + b;|] -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile7.ts b/tests/cases/fourslash/moveToFile7.ts index d5f7eab804f90..ef4d14a665261 100644 --- a/tests/cases/fourslash/moveToFile7.ts +++ b/tests/cases/fourslash/moveToFile7.ts @@ -15,7 +15,7 @@ ////[|const y: Date = p + b;|] ////a; y; -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile8_newFile.ts b/tests/cases/fourslash/moveToFile8_newFile.ts index 86d2b958b36e7..bd25cc0d5c236 100644 --- a/tests/cases/fourslash/moveToFile8_newFile.ts +++ b/tests/cases/fourslash/moveToFile8_newFile.ts @@ -9,7 +9,7 @@ ////[|const y: Date = p + b;|] ////a; y; -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment diff --git a/tests/cases/fourslash/moveToFile9_multipleStatements.ts b/tests/cases/fourslash/moveToFile9_multipleStatements.ts index 439d7ecfe857e..35a3868f8a95a 100644 --- a/tests/cases/fourslash/moveToFile9_multipleStatements.ts +++ b/tests/cases/fourslash/moveToFile9_multipleStatements.ts @@ -22,7 +22,7 @@ ////interface I {}|] ////t; -verify.moveToAnotherFile({ +verify.moveToFile({ newFileContents: { "/a.ts": `// header comment From 56032103304385682cedca46f9755c0755cafdc7 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sun, 26 Mar 2023 15:57:09 -0700 Subject: [PATCH 09/42] Resolving merge conflict changes --- src/compiler/diagnosticMessages.json | 2 +- src/services/services.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index b6055bacdd439..4fd7476b0b4e7 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7602,7 +7602,7 @@ }, "Move to file": { "category": "Message", - "code": 95176 + "code": 95178 }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { diff --git a/src/services/services.ts b/src/services/services.ts index 4cdf552883bc5..adb21eab33d74 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -64,6 +64,9 @@ import { EntityName, equateValues, ExportDeclaration, + extensionFromPath, + extensionIsTS, + fileExtensionIs, FileReference, FileTextChanges, filter, @@ -118,6 +121,7 @@ import { hasStaticModifier, hasSyntacticModifier, hasTabstop, + hasTSFileExtension, HostCancellationToken, hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, @@ -2925,14 +2929,14 @@ export function createLanguageService( const sourceFile = getValidSourceFile(fileName); const program = getProgram(); const allFiles = program?.getSourceFiles().filter(sourceFile => !program?.isSourceFileFromExternalLibrary(sourceFile)).map(f => f.fileName); - const extension = ts.extensionFromPath(fileName); + const extension = extensionFromPath(fileName); const files: string[] = []; if (allFiles) { for (const file of allFiles) { - if (ts.extensionIsTS(extension) && ts.hasTSFileExtension(file)) { + if (extensionIsTS(extension) && hasTSFileExtension(file)) { files.push(file); } - else if (ts.fileExtensionIs(file, extension)){ + else if (fileExtensionIs(file, extension)){ files.push(file); } } From dffe218caaeb8e8f3c340ac42a9bf67c594bbead Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sun, 26 Mar 2023 17:09:28 -0700 Subject: [PATCH 10/42] all tests pass --- src/services/createNewFilename.ts | 4 +- src/services/refactors/moveToFile.ts | 6 +- .../reference/api/tsserverlibrary.d.ts | 42 ++++ tests/baselines/reference/api/typescript.d.ts | 5 + ...excluding-node_modules-within-a-project.js | 114 +++------- ...r-a-file-belonging-to-multiple-projects.js | 82 ++----- ...-for-an-inferred-project-for-a-.js-file.js | 200 ++++++++++-------- ...-for-an-inferred-project-for-a-.ts-file.js | 88 +++----- .../works-with-different-.ts-extensions.js | 108 +++------- .../works-with-different-extensions.js | 100 +++------ ...es-moving-statement-to-an-existing-file.js | 130 ++++++++++++ tests/cases/fourslash/moveToFile2.ts | 6 +- tests/cases/fourslash/moveToFile4.ts | 29 +-- tests/cases/fourslash/moveToFile5.ts | 8 +- tests/cases/fourslash/moveToFile6.ts | 20 +- tests/cases/fourslash/moveToFile7.ts | 44 ---- ...ile8_newFile.ts => moveToFile7_newFile.ts} | 0 ...s.ts => moveToFile8_multipleStatements.ts} | 0 .../fourslash/refactorKind_moveToNewFile.ts | 3 +- 19 files changed, 467 insertions(+), 522 deletions(-) create mode 100644 tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js delete mode 100644 tests/cases/fourslash/moveToFile7.ts rename tests/cases/fourslash/{moveToFile8_newFile.ts => moveToFile7_newFile.ts} (100%) rename tests/cases/fourslash/{moveToFile9_multipleStatements.ts => moveToFile8_multipleStatements.ts} (100%) diff --git a/src/services/createNewFilename.ts b/src/services/createNewFilename.ts index a08f964862945..7596e7c21a378 100644 --- a/src/services/createNewFilename.ts +++ b/src/services/createNewFilename.ts @@ -127,7 +127,7 @@ function isPureImport(node: Node): boolean { case SyntaxKind.ImportEqualsDeclaration: return !hasSyntacticModifier(node, ModifierFlags.Export); case SyntaxKind.VariableStatement: - return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); + return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*requireStringLiteralLikeArgument*/ true)); default: return false; } @@ -265,7 +265,7 @@ function isInImport(decl: Declaration) { } function isVariableDeclarationInImport(decl: VariableDeclaration) { return isSourceFile(decl.parent.parent.parent) && - !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); + !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); } function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 27a670c3288b7..8a219ab3a7426 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -257,7 +257,7 @@ function isPureImport(node: Node): boolean { case SyntaxKind.ImportEqualsDeclaration: return !hasSyntacticModifier(node, ModifierFlags.Export); case SyntaxKind.VariableStatement: - return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*checkArgumentIsStringLiteralLike*/ true)); + return (node as VariableStatement).declarationList.declarations.every(d => !!d.initializer && isRequireCall(d.initializer, /*requireStringLiteralLikeArgument*/ true)); default: return false; } @@ -504,7 +504,7 @@ function forEachImportInStatement(statement: Statement, cb: (importNode: Support } else if (isVariableStatement(statement)) { for (const decl of statement.declarationList.declarations) { - if (decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true)) { + if (decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true)) { cb(decl as SupportedImport); } } @@ -753,7 +753,7 @@ function isInImport(decl: Declaration) { } function isVariableDeclarationInImport(decl: VariableDeclaration) { return isSourceFile(decl.parent.parent.parent) && - !!decl.initializer && isRequireCall(decl.initializer, /*checkArgumentIsStringLiteralLike*/ true); + !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); } function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 760a948efc21a..5fbca1cf9a1f8 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -146,6 +146,8 @@ declare namespace ts { GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", + GetEditsForMoveToFileRefactor = "getEditsForMoveToFileRefactor", + GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", ConfigurePlugin = "configurePlugin", @@ -501,6 +503,27 @@ declare namespace ts { interface GetApplicableRefactorsResponse extends Response { body?: ApplicableRefactorInfo[]; } + /** + * Request refactorings at a given position or selection area to move to an existing file. + */ + interface GetMoveToRefactoringFileSuggestionsRequest extends Request { + command: CommandTypes.GetMoveToRefactoringFileSuggestions; + arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; + } + type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { + triggerReason?: RefactorTriggerReason; + 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 + */ + interface GetMoveToRefactoringFileSuggestionsResponse extends Response { + body?: { + newFileName: string; + files: string[]; + }; + } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. */ @@ -564,6 +587,18 @@ declare namespace ts { interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; } + interface GetEditsForMoveToFileRefactorRequest extends Request { + command: CommandTypes.GetEditsForMoveToFileRefactor; + arguments: GetEditsForMoveToFileRefactorRequestArgs; + } + interface GetEditsForMoveToFileRefactorResponse extends Response { + body?: RefactorEditInfo; + } + type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeRequestArgs & { + refactor: string; + action: string; + filepath: string; + }; interface RefactorEditInfo { edits: FileCodeEdits[]; /** @@ -3907,6 +3942,8 @@ declare namespace ts { private getRange; private getApplicableRefactors; private getEditsForRefactor; + private getMoveToRefactoringFileSuggestions; + private getEditsForMoveToFileForRefactor; private organizeImports; private getEditsForFileRename; private getCodeFixes; @@ -10039,7 +10076,12 @@ declare namespace ts { /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { + newFilename: string | undefined; + files: string[] | undefined; + }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 85d2a5b10a201..9ae4db3008b79 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6169,7 +6169,12 @@ declare namespace ts { /** @deprecated `fileName` will be ignored */ applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { + newFilename: string | undefined; + files: string[] | undefined; + }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; + getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js index 2d5074fc8379a..d1c9edcfb3b5f 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:36.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:37.000] request: - { - "command": "open", - "arguments": { - "file": "/project/a/file1.ts" - }, - "seq": 1, - "type": "request" - } Before request //// [/project/a/file1.ts] interface ka { @@ -35,12 +27,15 @@ export const value1 = 0; {} -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:37.000] request: + { + "command": "open", + "arguments": { + "file": "/project/a/file1.ts" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:38.000] Search path: /project/a Info 3 [00:00:39.000] For info: /project/a/file1.ts :: Config file name: /project/tsconfig.json Info 4 [00:00:40.000] Creating configuration project /project/tsconfig.json @@ -72,12 +67,12 @@ Info 19 [00:00:55.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /pr Info 20 [00:00:56.000] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 21 [00:00:57.000] Project '/project/tsconfig.json' (Configured) Info 22 [00:00:58.000] Files (6) - /project/a/file1.ts - /project/node_modules/@types/node/someFile.d.ts - /project/node_modules/.cache/someFile.d.ts - /project/a/file4.ts - /project/b/file2.ts - /project/d/e/file3.ts + /project/a/file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /project/node_modules/@types/node/someFile.d.ts Text-1 "export const value = 0;" + /project/node_modules/.cache/someFile.d.ts Text-1 "export const value1 = 0;" + /project/a/file4.ts Text-1 "import { value } from \"../node_modules/@types/node/someFile.d.ts\";\nimport { value1 } from \"../node_modules/.cache/someFile.d.ts\";" + /project/b/file2.ts Text-1 "" + /project/d/e/file3.ts Text-1 "" a/file1.ts @@ -101,34 +96,36 @@ Info 24 [00:01:02.000] ----------------------------------------------- Info 24 [00:01:03.000] Open files: Info 24 [00:01:04.000] FileName: /project/a/file1.ts ProjectRootPath: undefined Info 24 [00:01:05.000] Projects: /project/tsconfig.json +Info 24 [00:01:06.000] response: + { + "responseRequired": false + } After request PolledWatches:: -/a/lib/lib.d.ts: +/a/lib/lib.d.ts: *new* {"pollingInterval":500} FsWatches:: -/project/tsconfig.json: +/project/tsconfig.json: *new* {} -/project/a/file4.ts: +/project/a/file4.ts: *new* {} -/project/b/file2.ts: +/project/b/file2.ts: *new* {} -/project/d/e/file3.ts: +/project/d/e/file3.ts: *new* {} FsWatchesRecursive:: -/project: +/project: *new* {} -/project/node_modules: +/project/node_modules: *new* {} -/project/node_modules/@types: +/project/node_modules/@types: *new* {} -Info 24 [00:01:06.000] response: - { - "responseRequired": false - } +Before request + Info 25 [00:01:07.000] request: { "command": "getMoveToRefactoringFileSuggestions", @@ -140,54 +137,6 @@ Info 25 [00:01:07.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/project/tsconfig.json: - {} -/project/a/file4.ts: - {} -/project/b/file2.ts: - {} -/project/d/e/file3.ts: - {} - -FsWatchesRecursive:: -/project: - {} -/project/node_modules: - {} -/project/node_modules/@types: - {} - -After request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/project/tsconfig.json: - {} -/project/a/file4.ts: - {} -/project/b/file2.ts: - {} -/project/d/e/file3.ts: - {} - -FsWatchesRecursive:: -/project: - {} -/project/node_modules: - {} -/project/node_modules/@types: - {} - Info 26 [00:01:08.000] response: { "response": { @@ -200,4 +149,5 @@ Info 26 [00:01:08.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js index f925ebb3a5506..019f6c912ce43 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-a-file-belonging-to-multiple-projects.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:19.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:20.000] request: - { - "command": "open", - "arguments": { - "file": "/blah/file1.ts" - }, - "seq": 1, - "type": "request" - } Before request //// [/blah/file1.ts] class CC { } @@ -28,12 +20,15 @@ import { value1 } from "../blah/file1.ts"; { "files": ["./file3.ts", "./file4.ts"] } -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:20.000] request: + { + "command": "open", + "arguments": { + "file": "/blah/file1.ts" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:21.000] Search path: /blah Info 3 [00:00:22.000] For info: /blah/file1.ts :: Config file name: /blah/tsconfig.json Info 4 [00:00:23.000] Creating configuration project /blah/tsconfig.json @@ -55,8 +50,8 @@ Info 11 [00:00:30.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /bl Info 12 [00:00:31.000] Finishing updateGraphWorker: Project: /blah/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 13 [00:00:32.000] Project '/blah/tsconfig.json' (Configured) Info 14 [00:00:33.000] Files (2) - /blah/file1.ts - /blah/file2.ts + /blah/file1.ts SVC-1-0 "class CC { }" + /blah/file2.ts Text-1 "" file1.ts @@ -72,26 +67,26 @@ Info 16 [00:00:37.000] ----------------------------------------------- Info 16 [00:00:38.000] Open files: Info 16 [00:00:39.000] FileName: /blah/file1.ts ProjectRootPath: undefined Info 16 [00:00:40.000] Projects: /blah/tsconfig.json +Info 16 [00:00:41.000] response: + { + "responseRequired": false + } After request PolledWatches:: -/a/lib/lib.d.ts: +/a/lib/lib.d.ts: *new* {"pollingInterval":500} -/blah/node_modules/@types: +/blah/node_modules/@types: *new* {"pollingInterval":500} FsWatches:: -/blah/tsconfig.json: +/blah/tsconfig.json: *new* {} -/blah/file2.ts: +/blah/file2.ts: *new* {} -FsWatchesRecursive:: +Before request -Info 16 [00:00:41.000] response: - { - "responseRequired": false - } Info 17 [00:00:42.000] request: { "command": "getMoveToRefactoringFileSuggestions", @@ -103,38 +98,6 @@ Info 17 [00:00:42.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} -/blah/node_modules/@types: - {"pollingInterval":500} - -FsWatches:: -/blah/tsconfig.json: - {} -/blah/file2.ts: - {} - -FsWatchesRecursive:: - -After request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} -/blah/node_modules/@types: - {"pollingInterval":500} - -FsWatches:: -/blah/tsconfig.json: - {} -/blah/file2.ts: - {} - -FsWatchesRecursive:: - Info 18 [00:00:43.000] response: { "response": { @@ -145,4 +108,5 @@ Info 18 [00:00:43.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js index f619e6226cba9..17da752e876eb 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.js-file.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:10.000] request: - { - "command": "open", - "arguments": { - "file": "/file2.js" - }, - "seq": 1, - "type": "request" - } Before request //// [/file1.js] import {} from "./file.js"; @@ -19,12 +11,15 @@ class C {} {"files":["./file1.js","./file.js"]} -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.js" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:11.000] Search path: / Info 3 [00:00:12.000] For info: /file2.js :: Config file name: /jsconfig.json Info 4 [00:00:13.000] Creating configuration project /jsconfig.json @@ -54,36 +49,112 @@ Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 un Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /jsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 16 [00:00:25.000] Project '/jsconfig.json' (Configured) Info 17 [00:00:26.000] Files (1) - /file1.js + /file1.js Text-1 "import {} from \"./file.js\";" file1.js Part of 'files' list in tsconfig.json Info 18 [00:00:27.000] ----------------------------------------------- -Info 19 [00:00:28.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* -Info 20 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file -Info 21 [00:00:30.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 22 [00:00:31.000] Project '/dev/null/inferredProject1*' (Inferred) -Info 23 [00:00:32.000] Files (1) - /file2.js +TI:: Creating typing installer + +PolledWatches:: +/file.js: *new* + {"pollingInterval":500} +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/jsconfig.json: *new* + {} +/file1.js: *new* + {} +/: *new* + {} + +TI:: [00:00:28.000] Global cache location '/a/data/', safe file path '/safeList.json', types map path /typesMap.json +TI:: [00:00:29.000] Processing cache location '/a/data/' +TI:: [00:00:30.000] Trying to find '/a/data/package.json'... +TI:: [00:00:31.000] Finished processing cache location '/a/data/' +TI:: [00:00:32.000] Npm config file: /a/data/package.json +TI:: [00:00:33.000] Npm config file: '/a/data/package.json' is missing, creating new one... +Info 19 [00:00:36.000] DirectoryWatcher:: Triggered with a :: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +Info 20 [00:00:37.000] Elapsed:: *ms DirectoryWatcher:: Triggered with a :: WatchInfo: 0 undefined Project: /jsconfig.json WatchType: Failed Lookup Locations +TI:: [00:00:42.000] Updating types-registry npm package... +TI:: [00:00:43.000] npm install --ignore-scripts types-registry@latest +TI:: [00:00:50.000] TI:: Updated types-registry npm package +TI:: typing installer creation complete +//// [/a/data/package.json] +{ "private": true } + +//// [/a/data/node_modules/types-registry/index.json] +{ + "entries": {} +} + + +TI:: [00:00:51.000] Got install request {"projectName":"/jsconfig.json","fileNames":["/file1.js"],"compilerOptions":{"allowJs":true,"maxNodeModuleJsDepth":2,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"noEmit":true,"configFilePath":"/jsconfig.json","allowNonTsExtensions":true},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/","cachePath":"/a/data/","kind":"discover"} +TI:: [00:00:52.000] Request specifies cache path '/a/data/', loading cached information... +TI:: [00:00:53.000] Processing cache location '/a/data/' +TI:: [00:00:54.000] Cache location was already processed... +TI:: [00:00:55.000] Failed to load safelist from types map file '/typesMap.json' +TI:: [00:00:56.000] Explicitly included types: [] +TI:: [00:00:57.000] Inferred typings from unresolved imports: [] +TI:: [00:00:58.000] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:00:59.000] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:00.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components +TI:: [00:01:01.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:02.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:03.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules +TI:: [00:01:04.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:05.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /jsconfig.json watcher already invoked: false +TI:: [00:01:06.000] Sending response: + {"projectName":"/jsconfig.json","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"allowJs":true,"maxNodeModuleJsDepth":2,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"noEmit":true,"configFilePath":"/jsconfig.json","allowNonTsExtensions":true},"typings":[],"unresolvedImports":[],"kind":"action::set"} +TI:: [00:01:07.000] No new typings were requested as a result of typings discovery +Info 21 [00:01:08.000] Starting updateGraphWorker: Project: /dev/null/inferredProject1* +Info 22 [00:01:09.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /dev/null/inferredProject1* WatchType: Missing file +Info 23 [00:01:10.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 24 [00:01:11.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 25 [00:01:12.000] Files (1) + /file2.js SVC-1-0 "class C {}" file2.js Root file specified for compilation -Info 24 [00:00:33.000] ----------------------------------------------- -Info 25 [00:00:34.000] Project '/jsconfig.json' (Configured) -Info 25 [00:00:35.000] Files (1) - -Info 25 [00:00:36.000] ----------------------------------------------- -Info 25 [00:00:37.000] Project '/dev/null/inferredProject1*' (Inferred) -Info 25 [00:00:38.000] Files (1) - -Info 25 [00:00:39.000] ----------------------------------------------- -Info 25 [00:00:40.000] Open files: -Info 25 [00:00:41.000] FileName: /file2.js ProjectRootPath: undefined -Info 25 [00:00:42.000] Projects: /dev/null/inferredProject1* +Info 26 [00:01:13.000] ----------------------------------------------- +TI:: [00:01:14.000] Got install request {"projectName":"/dev/null/inferredProject1*","fileNames":["/file2.js"],"compilerOptions":{"target":1,"jsx":1,"allowNonTsExtensions":true,"allowJs":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/","cachePath":"/a/data/","kind":"discover"} +TI:: [00:01:15.000] Request specifies cache path '/a/data/', loading cached information... +TI:: [00:01:16.000] Processing cache location '/a/data/' +TI:: [00:01:17.000] Cache location was already processed... +TI:: [00:01:18.000] Explicitly included types: [] +TI:: [00:01:19.000] Inferred typings from unresolved imports: [] +TI:: [00:01:20.000] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:21.000] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/bower_components","/node_modules"]} +TI:: [00:01:22.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components +TI:: [00:01:23.000] DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:24.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:25.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules +TI:: [00:01:26.000] DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:27.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false +TI:: [00:01:28.000] Sending response: + {"projectName":"/dev/null/inferredProject1*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"target":1,"jsx":1,"allowNonTsExtensions":true,"allowJs":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":[],"unresolvedImports":[],"kind":"action::set"} +TI:: [00:01:29.000] No new typings were requested as a result of typings discovery +Info 27 [00:01:30.000] Project '/jsconfig.json' (Configured) +Info 27 [00:01:31.000] Files (1) + +Info 27 [00:01:32.000] ----------------------------------------------- +Info 27 [00:01:33.000] Project '/dev/null/inferredProject1*' (Inferred) +Info 27 [00:01:34.000] Files (1) + +Info 27 [00:01:35.000] ----------------------------------------------- +Info 27 [00:01:36.000] Open files: +Info 27 [00:01:37.000] FileName: /file2.js ProjectRootPath: undefined +Info 27 [00:01:38.000] Projects: /dev/null/inferredProject1* +Info 27 [00:01:39.000] response: + { + "responseRequired": false + } After request PolledWatches:: @@ -91,9 +162,9 @@ PolledWatches:: {"pollingInterval":500} /a/lib/lib.d.ts: {"pollingInterval":500} -/bower_components: +/bower_components: *new* {"pollingInterval":500} -/node_modules: +/node_modules: *new* {"pollingInterval":500} FsWatches:: @@ -104,13 +175,9 @@ FsWatches:: /: {} -FsWatchesRecursive:: +Before request -Info 25 [00:00:43.000] response: - { - "responseRequired": false - } -Info 26 [00:00:44.000] request: +Info 28 [00:01:40.000] request: { "command": "getMoveToRefactoringFileSuggestions", "arguments": { @@ -121,51 +188,7 @@ Info 26 [00:00:44.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/file.js: - {"pollingInterval":500} -/a/lib/lib.d.ts: - {"pollingInterval":500} -/bower_components: - {"pollingInterval":500} -/node_modules: - {"pollingInterval":500} - -FsWatches:: -/jsconfig.json: - {} -/file1.js: - {} -/: - {} - -FsWatchesRecursive:: - -After request - -PolledWatches:: -/file.js: - {"pollingInterval":500} -/a/lib/lib.d.ts: - {"pollingInterval":500} -/bower_components: - {"pollingInterval":500} -/node_modules: - {"pollingInterval":500} - -FsWatches:: -/jsconfig.json: - {} -/file1.js: - {} -/: - {} - -FsWatchesRecursive:: - -Info 27 [00:00:45.000] response: +Info 29 [00:01:41.000] response: { "response": { "newFilename": "/C.js", @@ -174,4 +197,5 @@ Info 27 [00:00:45.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js index 280b473102d1e..5294fb0b108bb 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-for-an-inferred-project-for-a-.ts-file.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:09.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:10.000] request: - { - "command": "open", - "arguments": { - "file": "/file2.ts" - }, - "seq": 1, - "type": "request" - } Before request //// [/file1.ts] import {} from "./file.ts"; @@ -22,12 +14,15 @@ interface ka { {"files":["./file1.ts","./file.ts"]} -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:10.000] request: + { + "command": "open", + "arguments": { + "file": "/file2.ts" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:11.000] Search path: / Info 3 [00:00:12.000] For info: /file2.ts :: Config file name: /tsconfig.json Info 4 [00:00:13.000] Creating configuration project /tsconfig.json @@ -52,7 +47,7 @@ Info 14 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 un Info 15 [00:00:24.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 16 [00:00:25.000] Project '/tsconfig.json' (Configured) Info 17 [00:00:26.000] Files (1) - /file1.ts + /file1.ts Text-1 "import {} from \"./file.ts\";" file1.ts @@ -64,7 +59,7 @@ Info 20 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 un Info 21 [00:00:30.000] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 22 [00:00:31.000] Project '/dev/null/inferredProject1*' (Inferred) Info 23 [00:00:32.000] Files (1) - /file2.ts + /file2.ts SVC-1-0 "interface ka {\n name: string;\n }\n " file2.ts @@ -82,28 +77,28 @@ Info 25 [00:00:39.000] ----------------------------------------------- Info 25 [00:00:40.000] Open files: Info 25 [00:00:41.000] FileName: /file2.ts ProjectRootPath: undefined Info 25 [00:00:42.000] Projects: /dev/null/inferredProject1* +Info 25 [00:00:43.000] response: + { + "responseRequired": false + } After request PolledWatches:: -/file.ts: +/file.ts: *new* {"pollingInterval":500} -/a/lib/lib.d.ts: +/a/lib/lib.d.ts: *new* {"pollingInterval":500} FsWatches:: -/tsconfig.json: +/tsconfig.json: *new* {} -/file1.ts: +/file1.ts: *new* {} -/: +/: *new* {} -FsWatchesRecursive:: +Before request -Info 25 [00:00:43.000] response: - { - "responseRequired": false - } Info 26 [00:00:44.000] request: { "command": "getMoveToRefactoringFileSuggestions", @@ -115,42 +110,6 @@ Info 26 [00:00:44.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/file.ts: - {"pollingInterval":500} -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file1.ts: - {} -/: - {} - -FsWatchesRecursive:: - -After request - -PolledWatches:: -/file.ts: - {"pollingInterval":500} -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file1.ts: - {} -/: - {} - -FsWatchesRecursive:: - Info 27 [00:00:45.000] response: { "response": { @@ -160,4 +119,5 @@ Info 27 [00:00:45.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js index b4c9125449ee0..3946f44852f49 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-.ts-extensions.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:17.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:18.000] request: - { - "command": "open", - "arguments": { - "file": "/file1.ts" - }, - "seq": 1, - "type": "request" - } Before request //// [/file1.ts] interface ka { @@ -34,12 +26,15 @@ interface ka { {"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts"]} -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:18.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.ts" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:19.000] Search path: / Info 3 [00:00:20.000] For info: /file1.ts :: Config file name: /tsconfig.json Info 4 [00:00:21.000] Creating configuration project /tsconfig.json @@ -67,12 +62,12 @@ Info 13 [00:00:30.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 un Info 14 [00:00:31.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 15 [00:00:32.000] Project '/tsconfig.json' (Configured) Info 16 [00:00:33.000] Files (6) - /file1.ts - /file2.tsx - /file3.mts - /file4.cts - /file5.js - /file6.d.ts + /file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /file2.tsx Text-1 "" + /file3.mts Text-1 "" + /file4.cts Text-1 "" + /file5.js Text-1 "" + /file6.d.ts Text-1 "" file1.ts @@ -96,32 +91,32 @@ Info 18 [00:00:37.000] ----------------------------------------------- Info 18 [00:00:38.000] Open files: Info 18 [00:00:39.000] FileName: /file1.ts ProjectRootPath: undefined Info 18 [00:00:40.000] Projects: /tsconfig.json +Info 18 [00:00:41.000] response: + { + "responseRequired": false + } After request PolledWatches:: -/a/lib/lib.d.ts: +/a/lib/lib.d.ts: *new* {"pollingInterval":500} FsWatches:: -/tsconfig.json: +/tsconfig.json: *new* {} -/file2.tsx: +/file2.tsx: *new* {} -/file3.mts: +/file3.mts: *new* {} -/file4.cts: +/file4.cts: *new* {} -/file5.js: +/file5.js: *new* {} -/file6.d.ts: +/file6.d.ts: *new* {} -FsWatchesRecursive:: +Before request -Info 18 [00:00:41.000] response: - { - "responseRequired": false - } Info 19 [00:00:42.000] request: { "command": "getMoveToRefactoringFileSuggestions", @@ -133,50 +128,6 @@ Info 19 [00:00:42.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file2.tsx: - {} -/file3.mts: - {} -/file4.cts: - {} -/file5.js: - {} -/file6.d.ts: - {} - -FsWatchesRecursive:: - -After request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file2.tsx: - {} -/file3.mts: - {} -/file4.cts: - {} -/file5.js: - {} -/file6.d.ts: - {} - -FsWatchesRecursive:: - Info 20 [00:00:43.000] response: { "response": { @@ -190,4 +141,5 @@ Info 20 [00:00:43.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js index e2d2a27ad69a8..c39d3b43ed599 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-with-different-extensions.js @@ -1,13 +1,5 @@ +currentDirectory:: / useCaseSensitiveFileNames: false Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist -Info 1 [00:00:16.000] request: - { - "command": "open", - "arguments": { - "file": "/file1.js" - }, - "seq": 1, - "type": "request" - } Before request //// [/file1.js] class C {} @@ -28,12 +20,15 @@ class C {} {"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} -PolledWatches:: - -FsWatches:: - -FsWatchesRecursive:: - +Info 1 [00:00:16.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.js" + }, + "seq": 1, + "type": "request" + } Info 2 [00:00:17.000] Search path: / Info 3 [00:00:18.000] For info: /file1.js :: Config file name: /tsconfig.json Info 4 [00:00:19.000] Creating configuration project /tsconfig.json @@ -59,11 +54,11 @@ Info 12 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 un Info 13 [00:00:28.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info 14 [00:00:29.000] Project '/tsconfig.json' (Configured) Info 15 [00:00:30.000] Files (5) - /file1.js - /file2.js - /file3.mts - /file4.ts - /file5.js + /file1.js SVC-1-0 "class C {}" + /file2.js Text-1 "" + /file3.mts Text-1 "" + /file4.ts Text-1 "" + /file5.js Text-1 "" file1.js @@ -85,30 +80,30 @@ Info 17 [00:00:34.000] ----------------------------------------------- Info 17 [00:00:35.000] Open files: Info 17 [00:00:36.000] FileName: /file1.js ProjectRootPath: undefined Info 17 [00:00:37.000] Projects: /tsconfig.json +Info 17 [00:00:38.000] response: + { + "responseRequired": false + } After request PolledWatches:: -/a/lib/lib.d.ts: +/a/lib/lib.d.ts: *new* {"pollingInterval":500} FsWatches:: -/tsconfig.json: +/tsconfig.json: *new* {} -/file2.js: +/file2.js: *new* {} -/file3.mts: +/file3.mts: *new* {} -/file4.ts: +/file4.ts: *new* {} -/file5.js: +/file5.js: *new* {} -FsWatchesRecursive:: +Before request -Info 17 [00:00:38.000] response: - { - "responseRequired": false - } Info 18 [00:00:39.000] request: { "command": "getMoveToRefactoringFileSuggestions", @@ -120,46 +115,6 @@ Info 18 [00:00:39.000] request: "seq": 2, "type": "request" } -Before request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file2.js: - {} -/file3.mts: - {} -/file4.ts: - {} -/file5.js: - {} - -FsWatchesRecursive:: - -After request - -PolledWatches:: -/a/lib/lib.d.ts: - {"pollingInterval":500} - -FsWatches:: -/tsconfig.json: - {} -/file2.js: - {} -/file3.mts: - {} -/file4.ts: - {} -/file5.js: - {} - -FsWatchesRecursive:: - Info 19 [00:00:40.000] response: { "response": { @@ -171,4 +126,5 @@ Info 19 [00:00:40.000] response: ] }, "responseRequired": true - } \ No newline at end of file + } +After request diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js new file mode 100644 index 0000000000000..ef640e48f37fc --- /dev/null +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -0,0 +1,130 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:11.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/Foo/a.ts] +const x = 0; + +//// [/Foo/b.ts] +import {} from "./bar"; +const a = 1; + +//// [/Foo/tsconfig.json] +{ "files": ["./a.ts", "./b.ts"] } + + +Info 1 [00:00:12.000] request: + { + "command": "open", + "arguments": { + "file": "/Foo/a.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:13.000] Search path: /Foo +Info 3 [00:00:14.000] For info: /Foo/a.ts :: Config file name: /Foo/tsconfig.json +Info 4 [00:00:15.000] Creating configuration project /Foo/tsconfig.json +Info 5 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /Foo/tsconfig.json 2000 undefined Project: /Foo/tsconfig.json WatchType: Config file +Info 6 [00:00:17.000] Config: /Foo/tsconfig.json : { + "rootNames": [ + "/Foo/a.ts", + "/Foo/b.ts" + ], + "options": { + "configFilePath": "/Foo/tsconfig.json" + } +} +Info 7 [00:00:18.000] FileWatcher:: Added:: WatchInfo: /Foo/b.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:19.000] Starting updateGraphWorker: Project: /Foo/tsconfig.json +Info 9 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info 10 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info 11 [00:00:22.000] DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info 12 [00:00:23.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info 13 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /Foo/tsconfig.json WatchType: Missing file +Info 14 [00:00:25.000] DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots +Info 15 [00:00:26.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots +Info 16 [00:00:27.000] Finishing updateGraphWorker: Project: /Foo/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 17 [00:00:28.000] Project '/Foo/tsconfig.json' (Configured) +Info 18 [00:00:29.000] Files (2) + /Foo/a.ts SVC-1-0 "const x = 0;" + /Foo/b.ts Text-1 "import {} from \"./bar\";\nconst a = 1;" + + + a.ts + Part of 'files' list in tsconfig.json + b.ts + Part of 'files' list in tsconfig.json + +Info 19 [00:00:30.000] ----------------------------------------------- +Info 20 [00:00:31.000] Project '/Foo/tsconfig.json' (Configured) +Info 20 [00:00:32.000] Files (2) + +Info 20 [00:00:33.000] ----------------------------------------------- +Info 20 [00:00:34.000] Open files: +Info 20 [00:00:35.000] FileName: /Foo/a.ts ProjectRootPath: undefined +Info 20 [00:00:36.000] Projects: /Foo/tsconfig.json +Info 20 [00:00:37.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/foo/bar: *new* + {"pollingInterval":500} +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} +/foo/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/foo/tsconfig.json: *new* + {} +/foo/b.ts: *new* + {} +/foo: *new* + {} + +Before request + +Info 21 [00:00:38.000] request: + { + "command": "getEditsForMoveToFileRefactor", + "arguments": { + "file": "/Foo/a.ts", + "startLine": 1, + "startOffset": 1, + "endLine": 2, + "endOffset": 12, + "refactor": "Move to file", + "action": "Move to file", + "filepath": "/Foo/b.ts" + }, + "seq": 2, + "type": "request" + } +Info 22 [00:00:39.000] response: + { + "response": { + "edits": [ + { + "fileName": "/Foo/a.ts", + "textChanges": [ + { + "start": { + "line": 1, + "offset": 1 + }, + "end": { + "line": 1, + "offset": 13 + }, + "newText": "" + } + ] + } + ] + }, + "responseRequired": true + } +After request diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile2.ts index 26167fde76ce8..26508373e9dcc 100644 --- a/tests/cases/fourslash/moveToFile2.ts +++ b/tests/cases/fourslash/moveToFile2.ts @@ -19,10 +19,10 @@ verify.moveToFile({ newFileContents: { "/a.ts":"", "/bar.ts": -` import { type A } from "./other"; +`import { type A } from "./other"; - const q = 0; - function f(a: A) { } +const q = 0; +function f(a: A) { } `, }, newFile: "/bar.ts", diff --git a/tests/cases/fourslash/moveToFile4.ts b/tests/cases/fourslash/moveToFile4.ts index 933c1b02b680b..d9b2b884ce721 100644 --- a/tests/cases/fourslash/moveToFile4.ts +++ b/tests/cases/fourslash/moveToFile4.ts @@ -1,34 +1,39 @@ - /// -// @Filename: /bar.ts -//// import { b } from './a'; +//@Filename: /bar.ts +////import './someFile'; //// -////const q = 0; +////const q = 20; // @Filename: /a.ts ////// header comment //// -////export const p = 0; -////export const b = 1; -////[|const y = p + b;|] - +////import './foo'; +////import { a, b } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] +// @Filename: /other.ts +////export const b = 2; +////export const a = 1; verify.moveToFile({ newFileContents: { "/a.ts": `// header comment +import './foo'; +import { a } from './other'; export const p = 0; -export const b = 1; `, "/bar.ts": -` import { b, p } from './a'; +`import { p } from './a'; +import { b } from './other'; +import './someFile'; -const q = 0; -const y = p + b; +const q = 20; +const y: Date = p + b; `, }, newFile: "/bar.ts", diff --git a/tests/cases/fourslash/moveToFile5.ts b/tests/cases/fourslash/moveToFile5.ts index d9b2b884ce721..d8279abef4c40 100644 --- a/tests/cases/fourslash/moveToFile5.ts +++ b/tests/cases/fourslash/moveToFile5.ts @@ -9,21 +9,17 @@ ////// header comment //// ////import './foo'; -////import { a, b } from './other'; +////import { a, b, alreadyUnused } from './other'; ////const p = 0; ////[|const y: Date = p + b;|] -// @Filename: /other.ts -////export const b = 2; -////export const a = 1; - verify.moveToFile({ newFileContents: { "/a.ts": `// header comment import './foo'; -import { a } from './other'; +import { a, alreadyUnused } from './other'; export const p = 0; `, diff --git a/tests/cases/fourslash/moveToFile6.ts b/tests/cases/fourslash/moveToFile6.ts index d8279abef4c40..ef4d14a665261 100644 --- a/tests/cases/fourslash/moveToFile6.ts +++ b/tests/cases/fourslash/moveToFile6.ts @@ -1,9 +1,10 @@ /// //@Filename: /bar.ts -////import './someFile'; -//// -////const q = 20; +////import './blah'; +////import './blah2'; +////const a = 2; +////a; // @Filename: /a.ts ////// header comment @@ -12,24 +13,27 @@ ////import { a, b, alreadyUnused } from './other'; ////const p = 0; ////[|const y: Date = p + b;|] +////a; y; verify.moveToFile({ newFileContents: { "/a.ts": `// header comment +import { y } from './bar'; import './foo'; import { a, alreadyUnused } from './other'; export const p = 0; -`, +a; y;`, "/bar.ts": `import { p } from './a'; +import './blah'; +import './blah2'; import { b } from './other'; -import './someFile'; - -const q = 20; -const y: Date = p + b; +const a = 2; +a; +export const y: Date = p + b; `, }, newFile: "/bar.ts", diff --git a/tests/cases/fourslash/moveToFile7.ts b/tests/cases/fourslash/moveToFile7.ts deleted file mode 100644 index ef4d14a665261..0000000000000 --- a/tests/cases/fourslash/moveToFile7.ts +++ /dev/null @@ -1,44 +0,0 @@ -/// - -//@Filename: /bar.ts -////import './blah'; -////import './blah2'; -////const a = 2; -////a; - -// @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const y: Date = p + b;|] -////a; y; - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`// header comment - -import { y } from './bar'; -import './foo'; -import { a, alreadyUnused } from './other'; -export const p = 0; -a; y;`, - - "/bar.ts": -`import { p } from './a'; -import './blah'; -import './blah2'; -import { b } from './other'; -const a = 2; -a; -export const y: Date = p + b; -`, - }, - newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile8_newFile.ts b/tests/cases/fourslash/moveToFile7_newFile.ts similarity index 100% rename from tests/cases/fourslash/moveToFile8_newFile.ts rename to tests/cases/fourslash/moveToFile7_newFile.ts diff --git a/tests/cases/fourslash/moveToFile9_multipleStatements.ts b/tests/cases/fourslash/moveToFile8_multipleStatements.ts similarity index 100% rename from tests/cases/fourslash/moveToFile9_multipleStatements.ts rename to tests/cases/fourslash/moveToFile8_multipleStatements.ts diff --git a/tests/cases/fourslash/refactorKind_moveToNewFile.ts b/tests/cases/fourslash/refactorKind_moveToNewFile.ts index d9b280f565222..7d039727e3f5e 100644 --- a/tests/cases/fourslash/refactorKind_moveToNewFile.ts +++ b/tests/cases/fourslash/refactorKind_moveToNewFile.ts @@ -5,7 +5,8 @@ goTo.select("a", "b"); verify.refactorKindAvailable("refactor.move", [ - "refactor.move.newFile" + "refactor.move.newFile", + "refactor.move.file" ], { allowTextChangesInNewFiles: true From 28d8258edd55f5df649de4b9b3e78f513daa4f1c Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sun, 26 Mar 2023 21:31:19 -0700 Subject: [PATCH 11/42] duplicate code cleanup --- src/server/protocol.ts | 4 - src/services/createNewFilename.ts | 52 +++-- src/services/refactors/moveToFile.ts | 319 +-------------------------- 3 files changed, 36 insertions(+), 339 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index ae21af1d5a435..96ca8bf89d469 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -616,11 +616,7 @@ export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRange */ export interface GetMoveToRefactoringFileSuggestionsResponse extends Response { body?: { - /// Suggested name if a new file is created newFileName: string; - - /// List of file paths that the selected text can be moved to - // TODO: should this be an object instead so that TS could customize how files are shown in the file picker UI? files: string[]; }; } diff --git a/src/services/createNewFilename.ts b/src/services/createNewFilename.ts index 7596e7c21a378..80c676edd4007 100644 --- a/src/services/createNewFilename.ts +++ b/src/services/createNewFilename.ts @@ -5,24 +5,28 @@ import { copyEntries, extensionFromPath, forEachEntry, getAssignmentDeclarationK import { createTextRangeFromSpan, Debug, getRefactorContextSpan, getSymbolId, isBinaryExpression, isBindingElement, isExpressionStatement, isIdentifier, isNamedDeclaration, isOmittedExpression, isSourceFile, isVariableDeclaration, rangeContainsRange, Symbol, symbolNameNoDefault } from "./_namespaces/ts"; import { LanguageServiceHost, RefactorContext } from "./types"; -interface ToMove { +/**@internal */ +export interface ToMove { readonly all: readonly Statement[]; readonly ranges: readonly StatementRange[]; } -interface StatementRange { +/**@internal */ +export interface StatementRange { readonly first: Statement; readonly afterLast: Statement | undefined; } -interface ReadonlySymbolSet { +/**@internal */ +export interface ReadonlySymbolSet { size(): number; has(symbol: Symbol): boolean; forEach(cb: (symbol: Symbol) => void): void; forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; } -interface UsageInfo { +/**@internal */ +export interface UsageInfo { // Symbols whose declarations are moved from the old file to the new file. readonly movedSymbols: ReadonlySymbolSet; @@ -36,7 +40,8 @@ interface UsageInfo { readonly unusedImportsFromOldFile: ReadonlySymbolSet; } type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' -type NonVariableTopLevelDeclaration = +/**@internal */ +export type NonVariableTopLevelDeclaration = | FunctionDeclaration | ClassDeclaration | EnumDeclaration @@ -45,8 +50,11 @@ type NonVariableTopLevelDeclaration = | ModuleDeclaration | TopLevelExpressionStatement | ImportEqualsDeclaration; -interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } -type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; + +/**@internal */ +export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } + /**@internal */ +export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; /** @internal */ export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { @@ -74,8 +82,11 @@ export function createNewFilename(oldFile: SourceFile, program: Program, context return ""; } -interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } -function getRangeToMove(context: RefactorContext): RangeToMove | undefined { +/**@internal */ +export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } + +/**@internal */ +export function getRangeToMove(context: RefactorContext): RangeToMove | undefined { const { file } = context; const range = createTextRangeFromSpan(getRefactorContextSpan(context)); const { statements } = file; @@ -100,7 +111,8 @@ function getRangeToMove(context: RefactorContext): RangeToMove | undefined { }; } -function getStatementsToMove(context: RefactorContext): ToMove | undefined { +/**@internal */ +export function getStatementsToMove(context: RefactorContext): ToMove | undefined { const rangeToMove = getRangeToMove(context); if (rangeToMove === undefined) return undefined; const all: Statement[] = []; @@ -133,7 +145,8 @@ function isPureImport(node: Node): boolean { } } -function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { +/** @internal */ +export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { const movedSymbols = new SymbolSet(); const oldImportsNeededByNewFile = new SymbolSet(); const newFileImportsFromOldFile = new SymbolSet(); @@ -225,7 +238,8 @@ function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Sym }); } -function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { +/**@internal */ +export function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { switch (statement.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: @@ -247,8 +261,8 @@ function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevel } } } - -function isInImport(decl: Declaration) { +/** @internal */ +export function isInImport(decl: Declaration) { switch (decl.kind) { case SyntaxKind.ImportEqualsDeclaration: case SyntaxKind.ImportSpecifier: @@ -268,7 +282,8 @@ function isVariableDeclarationInImport(decl: VariableDeclaration) { !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); } -function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { +/**@internal */ +export 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 { @@ -287,7 +302,8 @@ function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node } } -function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { +/**@internal */ +export function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { switch (node.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: @@ -301,7 +317,9 @@ function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLev return false; } } -interface ReadonlySymbolSet { + +/**@internal */ +export interface ReadonlySymbolSet { size(): number; has(symbol: Symbol): boolean; forEach(cb: (symbol: Symbol) => void): void; diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 8a219ab3a7426..442d402fd66e7 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -3,41 +3,28 @@ 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, ExternalModuleReference, factory, find, FindAllReferences, - findIndex, - firstDefined, flatMap, - forEachEntry, - FunctionDeclaration, - getAssignmentDeclarationKind, getBaseFileName, GetCanonicalFileName, getDecorators, @@ -46,10 +33,7 @@ import { getModifiers, getPropertySymbolFromBindingElement, getQuotePreference, - getRangesWhere, - getRefactorContextSpan, getRelativePathFromFile, - getSymbolId, getUniqueName, hasSyntacticModifier, hostGetCanonicalFileName, @@ -57,20 +41,15 @@ import { ImportDeclaration, ImportEqualsDeclaration, insertImports, - InterfaceDeclaration, InternalSymbolName, isArrayLiteralExpression, - isBinaryExpression, isBindingElement, - isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, - isNamedDeclaration, isObjectLiteralExpression, - isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, @@ -88,7 +67,6 @@ import { mapDefined, ModifierFlags, ModifierLike, - ModuleDeclaration, NamedImportBindings, Node, NodeFlags, @@ -96,10 +74,8 @@ import { normalizePath, ObjectBindingElementWithoutPropertyName, Program, - PropertyAccessExpression, PropertyAssignment, QuotePreference, - rangeContainsRange, RefactorContext, RefactorEditInfo, RequireOrImportCall, @@ -107,27 +83,23 @@ import { 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 { forEachTopLevelDeclaration, getStatementsToMove, getUsageInfo, isNonVariableTopLevelDeclaration, isTopLevelDeclaration, NonVariableTopLevelDeclaration, ReadonlySymbolSet, StatementRange, ToMove, TopLevelDeclaration, TopLevelVariableDeclaration, UsageInfo } from "../createNewFilename"; const refactorNameForMoveToFile = "Move to file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); @@ -162,32 +134,6 @@ registerRefactor(refactorNameForMoveToFile, { } }); -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(context: RefactorContext, oldFile: SourceFile, newFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); @@ -220,117 +166,6 @@ function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTrack } } -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 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) {//not needed - 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; - } -} - function getNewStatementsAndRemoveFromOldFile( oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder ) { @@ -720,42 +555,6 @@ function getNewFileImportsAndAddExportInOldFile( return copiedOldImports; } -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; -} - -// 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: { @@ -800,127 +599,13 @@ function filterBindingName(name: BindingName, keep: (name: Identifier) => boolea } } -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(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -class SymbolSet implements ReadonlySymbolSet { - private map = new Map(); - 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(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(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(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); } @@ -1016,5 +701,3 @@ function createExportAssignment(name: string): Statement { SyntaxKind.EqualsToken, factory.createIdentifier(name))); } - - From 6ff1f26400fa7d6f95d52ba6607b9c55733b67ca Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 28 Mar 2023 16:08:46 -0700 Subject: [PATCH 12/42] deleting 'triggerReason' property --- src/server/protocol.ts | 1 - src/server/session.ts | 2 +- src/services/refactors/moveToFile.ts | 2 +- src/services/services.ts | 4 ++-- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 96ca8bf89d469..d0ee4e6d1697f 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -607,7 +607,6 @@ export interface GetMoveToRefactoringFileSuggestionsRequest extends Request { arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; } export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { - triggerReason?: RefactorTriggerReason; kind?: string; }; /** diff --git a/src/server/session.ts b/src/server/session.ts index 44e333ae135ef..1621394e370fd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2710,7 +2710,7 @@ export class Session implements EventSender { if (project.containsFile(file)) { const scriptInfo = project.getScriptInfoForNormalizedPath(file); if (scriptInfo) { - const { newFilename, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file), args.triggerReason, args.kind); + const { newFilename, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); if (files) { for (const file of files) { if (!allFiles.includes(file)) { diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 442d402fd66e7..23d869d963860 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -194,7 +194,7 @@ function getNewStatementsAndRemoveFromOldFile( if (sourceFile && sourceFile.statements.length > 0) { changes.insertNodesAfter(sourceFile, sourceFile.statements[sourceFile.statements.length - 1], body); } - if (imports.length > 0 && sourceFile) { + if (sourceFile && imports.length > 0) { insertImports(changes, sourceFile, imports, /*blankLineBetween*/ true, preferences); } } diff --git a/src/services/services.ts b/src/services/services.ts index adb21eab33d74..613b4bf1ba8b4 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2924,7 +2924,7 @@ export function createLanguageService( return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); } - function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions, triggerReason: RefactorTriggerReason, kind: string): { newFilename: string | undefined, files: string[] | undefined } { + function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFilename: string | undefined, files: string[] | undefined } { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const program = getProgram(); @@ -2944,7 +2944,7 @@ export function createLanguageService( //creating new filename let newFilename; if (program) { - newFilename = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions, triggerReason, kind), host); + newFilename = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); } return { newFilename, files }; } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5fbca1cf9a1f8..7fb92c27374f3 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -511,7 +511,6 @@ declare namespace ts { arguments: GetMoveToRefactoringFileSuggestionsRequestArgs; } type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRangeRequestArgs & { - triggerReason?: RefactorTriggerReason; kind?: string; }; /** From 4f092541cc5594286502c06f8a2498d256c5ed4e Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 30 Mar 2023 10:34:38 -0700 Subject: [PATCH 13/42] added export and deleted duplicated code --- src/services/refactors/moveToFile.ts | 520 +----------------------- src/services/refactors/moveToNewFile.ts | 38 +- 2 files changed, 22 insertions(+), 536 deletions(-) diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 23d869d963860..d01fee4187823 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -1,105 +1,36 @@ -import { getModuleSpecifier } from "../../compiler/moduleSpecifiers"; import { AnyImportOrRequireStatement, append, ApplicableRefactorInfo, - BindingElement, - BindingName, - CallExpression, - canHaveDecorators, - canHaveModifiers, - canHaveSymbol, cast, codefix, - combinePaths, - concatenate, - contains, - createModuleSpecifierResolutionHost, Debug, - DeclarationStatement, Diagnostics, emptyArray, - escapeLeadingUnderscores, - Expression, - ExternalModuleReference, - factory, - find, - FindAllReferences, - flatMap, getBaseFileName, - GetCanonicalFileName, - getDecorators, - getDirectoryPath, getLocaleSpecificMessage, - getModifiers, - getPropertySymbolFromBindingElement, getQuotePreference, - getRelativePathFromFile, - getUniqueName, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, - ImportDeclaration, - ImportEqualsDeclaration, insertImports, - InternalSymbolName, - isArrayLiteralExpression, - isBindingElement, - isExpressionStatement, - isExternalModuleReference, - isIdentifier, - isImportDeclaration, - isImportEqualsDeclaration, - isObjectLiteralExpression, isPrologueDirective, - isPropertyAccessExpression, - isPropertyAssignment, - isRequireCall, - isSourceFile, - isStringLiteral, - isStringLiteralLike, - isVariableDeclaration, - isVariableDeclarationList, - isVariableStatement, LanguageServiceHost, - last, - length, - makeImportIfNecessary, - mapDefined, ModifierFlags, - ModifierLike, - NamedImportBindings, - Node, - NodeFlags, nodeSeenTracker, - normalizePath, - ObjectBindingElementWithoutPropertyName, Program, - PropertyAssignment, QuotePreference, RefactorContext, RefactorEditInfo, - RequireOrImportCall, - RequireVariableStatement, - resolvePath, - ScriptTarget, skipAlias, SourceFile, - Statement, - StringLiteralLike, - SymbolFlags, - symbolNameNoDefault, SyntaxKind, takeWhile, textChanges, - tryCast, TypeChecker, - TypeNode, UserPreferences, - VariableDeclaration, - VariableStatement, } from "../_namespaces/ts"; -import { registerRefactor } from "../_namespaces/ts.refactor"; -import { forEachTopLevelDeclaration, getStatementsToMove, getUsageInfo, isNonVariableTopLevelDeclaration, isTopLevelDeclaration, NonVariableTopLevelDeclaration, ReadonlySymbolSet, StatementRange, ToMove, TopLevelDeclaration, TopLevelVariableDeclaration, UsageInfo } from "../createNewFilename"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, registerRefactor, updateImportsInOtherFiles } from "../_namespaces/ts.refactor"; +import { getStatementsToMove, getUsageInfo, isTopLevelDeclaration, ReadonlySymbolSet, ToMove, UsageInfo } from "../createNewFilename"; const refactorNameForMoveToFile = "Move to file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); @@ -151,21 +82,6 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string } } -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, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder ) { @@ -185,7 +101,7 @@ function getNewStatementsAndRemoveFromOldFile( deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); deleteMovedStatements(oldFile, toMove.ranges, changes); - updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename); + updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename, quotePreference); const imports = getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); @@ -217,290 +133,6 @@ 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, -): 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, factory.createStringLiteral(newModuleSpecifier), shouldMove); - if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); - - const ns = getNamespaceLikeImport(importNode); - if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleSpecifier, ns, importNode); - }); - } - } -} - -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, -): 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)); - } -} - -function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node { - const newNamespaceId = factory.createIdentifier(newNamespaceName); - const newModuleString = factory.createStringLiteral(newModuleSpecifier); - 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(factory.createStringLiteral(pathToNewFileWithCorrectExtension))) 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, @@ -555,149 +187,3 @@ function getNewFileImportsAndAddExportInOldFile( return copiedOldImports; } -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; - } - } -} - -type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; - -function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { - Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); - return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); -} - -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(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))); -} diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 2fdff08a85282..a3dc866e7b042 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -254,7 +254,7 @@ function isPureImport(node: Node): boolean { } } -function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { +export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { const cfg = program.getCompilerOptions().configFile; if (!cfg) return; @@ -308,20 +308,20 @@ function getNewStatementsAndRemoveFromOldFile( ]; } -function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { +export 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) { +export 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( +export function updateImportsInOtherFiles( changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newFilename: string, quotePreference: QuotePreference ): void { const checker = program.getTypeChecker(); @@ -351,7 +351,7 @@ function updateImportsInOtherFiles( } } -function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { +export function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { switch (node.kind) { case SyntaxKind.ImportDeclaration: return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? @@ -365,7 +365,7 @@ function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { } } -function updateNamespaceLikeImport( +export function updateNamespaceLikeImport( changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, @@ -395,7 +395,7 @@ function updateNamespaceLikeImport( } } -function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { + function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { const newNamespaceId = factory.createIdentifier(newNamespaceName); const newModuleString = makeStringLiteral(newModuleSpecifier, quotePreference); switch (node.kind) { @@ -414,13 +414,13 @@ function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: } } -function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { +export 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 { +export function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { if (isImportDeclaration(statement)) { if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); } @@ -447,7 +447,7 @@ type SupportedImportStatement = | ImportEqualsDeclaration | VariableStatement; -function createOldFileImportsFromNewFile( +export function createOldFileImportsFromNewFile( sourceFile: SourceFile, newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, @@ -469,7 +469,7 @@ function createOldFileImportsFromNewFile( return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); } -function makeImportOrRequire( +export function makeImportOrRequire( sourceFile: SourceFile, defaultImport: Identifier | undefined, imports: readonly string[], @@ -503,7 +503,7 @@ 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[] { +export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { return flatMap(toMove, statement => { if (isTopLevelDeclarationStatement(statement) && !isExported(sourceFile, statement, useEs6Exports) && @@ -515,7 +515,7 @@ function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needEx }); } -function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { +export 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); @@ -756,7 +756,7 @@ function isVariableDeclarationInImport(decl: VariableDeclaration) { !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); } -function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { +export function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { switch (i.kind) { case SyntaxKind.ImportDeclaration: { const clause = i.importClause; @@ -859,7 +859,7 @@ type NonVariableTopLevelDeclaration = type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; -function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { +export function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); } @@ -921,11 +921,11 @@ function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node } } -function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { +export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); } -function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { +export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { switch (d.kind) { case SyntaxKind.VariableDeclaration: return d.parent.parent; @@ -937,7 +937,7 @@ function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclar } } -function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { +export 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); @@ -948,7 +948,7 @@ function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationSta } } -function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { +export 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)); } From 0866cfc4243c8f2e68b590aa71a39e0dda0b3b06 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 30 Mar 2023 11:56:40 -0700 Subject: [PATCH 14/42] move exports to new file, fix doChange as needed --- src/services/moveToFileAndNewFile.ts | 458 +++++++++++++ src/services/refactors/moveToFile.ts | 3 +- src/services/refactors/moveToNewFile.ts | 868 +----------------------- 3 files changed, 465 insertions(+), 864 deletions(-) create mode 100644 src/services/moveToFileAndNewFile.ts diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts new file mode 100644 index 0000000000000..0f124c4d84030 --- /dev/null +++ b/src/services/moveToFileAndNewFile.ts @@ -0,0 +1,458 @@ +import { cast, concatenate, contains, emptyArray, find, flatMap, GetCanonicalFileName, last,length,mapDefined,tryCast } from "../compiler/core"; +import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; +import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; +import { AnyImportOrRequireStatement, BindingElement, BindingName, CallExpression, DeclarationStatement, Expression, ExternalModuleReference, Identifier, ImportDeclaration, ImportEqualsDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, NamedImportBindings, Node, NodeFlags, Program, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TypeChecker, TypeNode, VariableDeclaration, VariableStatement } from "../compiler/types"; +import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, createModuleSpecifierResolutionHost, Debug, escapeLeadingUnderscores, factory,FindAllReferences,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBindingElement, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, skipAlias, symbolNameNoDefault, textChanges } from "./_namespaces/ts"; +import { forEachTopLevelDeclaration, isNonVariableTopLevelDeclaration, NonVariableTopLevelDeclaration, ReadonlySymbolSet,StatementRange, TopLevelDeclaration, TopLevelVariableDeclaration } from "./createNewFilename"; + +export 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); + } +} + +export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { + for (const { first, afterLast } of moved) { + changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); + } +} + +export 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)!))); + } +} + +export 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); + }); + } + } +} + +export 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}`); + } +} + +export 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 createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { + return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); +} + +export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { + return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression + : i.initializer.arguments[0]); +} + +export 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 }; +export type SupportedImportStatement = + | ImportDeclaration + | ImportEqualsDeclaration + | VariableStatement; + +export 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); +} + +export 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)); +} + +export 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; + }); +} + +export 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))); +} + +export 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; + } +} + +type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; + +function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { + Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); + return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); +} + +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(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)]; +} + +/** 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))); +} + +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}`); + } +} + +export 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; + } + } +} + +export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { + return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); +} + +export 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; + } +} + +export 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)); + } +} diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index d01fee4187823..8eb3d6c39a117 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -29,8 +29,9 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, registerRefactor, updateImportsInOtherFiles } from "../_namespaces/ts.refactor"; import { getStatementsToMove, getUsageInfo, isTopLevelDeclaration, ReadonlySymbolSet, ToMove, UsageInfo } from "../createNewFilename"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, updateImportsInOtherFiles } from "../moveToFileAndNewFile"; +import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index a3dc866e7b042..344e3f71f0897 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -1,135 +1,34 @@ -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, 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 { createNewFilename, getStatementsToMove, getUsageInfo, isTopLevelDeclaration,ReadonlySymbolSet, ToMove, UsageInfo } from "../createNewFilename"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, SupportedImportStatement, updateImportsInOtherFiles } from "../moveToFileAndNewFile"; const refactorName = "Move to a new file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); @@ -156,55 +55,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,63 +72,6 @@ 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; - } -} - -export 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, ) { @@ -308,291 +111,6 @@ function getNewStatementsAndRemoveFromOldFile( ]; } -export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { - for (const { first, afterLast } of moved) { - changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); - } -} - -export 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)!))); - } -} - -export 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); - }); - } - } -} - -export 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}`); - } -} - -export 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}`); - } -} - -export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { - return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression - : i.initializer.arguments[0]); -} - -export 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; - -export 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); -} - -export 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]); -} - -export 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; - }); -} - -export 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, @@ -640,379 +158,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); -} - -export 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(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -class SymbolSet implements ReadonlySymbolSet { - private map = new Map(); - 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(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; -export 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(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(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}`); - } -} - -export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { - return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); -} - -export 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; - } -} - -export 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)); - } -} - -export 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(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))); -} From 6884d006740b66752bbd27e88e774fd8bb832dad Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 30 Mar 2023 12:26:04 -0700 Subject: [PATCH 15/42] combining createnewFile with moveToFileAndNewFile --- src/services/createNewFilename.ts | 354 ---------------------- src/services/moveToFileAndNewFile.ts | 379 +++++++++++++++++++++++- src/services/refactors/moveToFile.ts | 3 +- src/services/refactors/moveToNewFile.ts | 3 +- src/services/services.ts | 2 +- 5 files changed, 377 insertions(+), 364 deletions(-) delete mode 100644 src/services/createNewFilename.ts diff --git a/src/services/createNewFilename.ts b/src/services/createNewFilename.ts deleted file mode 100644 index 80c676edd4007..0000000000000 --- a/src/services/createNewFilename.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { cast, contains, find, findIndex, firstDefined, getRangesWhere, some } from "../compiler/core"; -import { combinePaths, getDirectoryPath } from "../compiler/path"; -import { AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, ClassDeclaration, Declaration, EnumDeclaration, ExpressionStatement, FunctionDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, ModifierFlags, ModuleDeclaration, Node, Program, PropertyAccessExpression, SourceFile, Statement, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; -import { copyEntries, extensionFromPath, forEachEntry, getAssignmentDeclarationKind, hasSyntacticModifier, isDeclarationName, isPrologueDirective, isRequireCall } from "../compiler/utilities"; -import { createTextRangeFromSpan, Debug, getRefactorContextSpan, getSymbolId, isBinaryExpression, isBindingElement, isExpressionStatement, isIdentifier, isNamedDeclaration, isOmittedExpression, isSourceFile, isVariableDeclaration, rangeContainsRange, Symbol, symbolNameNoDefault } from "./_namespaces/ts"; -import { LanguageServiceHost, RefactorContext } from "./types"; - -/**@internal */ -export interface ToMove { - readonly all: readonly Statement[]; - readonly ranges: readonly StatementRange[]; -} - -/**@internal */ -export interface StatementRange { - readonly first: Statement; - readonly afterLast: Statement | undefined; -} - -/**@internal */ -export interface ReadonlySymbolSet { - size(): number; - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -/**@internal */ -export 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; -} -type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' -/**@internal */ -export type NonVariableTopLevelDeclaration = - | FunctionDeclaration - | ClassDeclaration - | EnumDeclaration - | TypeAliasDeclaration - | InterfaceDeclaration - | ModuleDeclaration - | TopLevelExpressionStatement - | ImportEqualsDeclaration; - -/**@internal */ -export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } - /**@internal */ -export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; - -/** @internal */ -export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { - const checker = program.getTypeChecker(); - const toMove = getStatementsToMove(context); - let usage; - if (toMove) { - 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; - return newFilename; - } - return ""; -} - -/**@internal */ -export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } - -/**@internal */ -export 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], - }; -} - -/**@internal */ -export 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; - } -} - -/** @internal */ -export 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; - } -} - -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"; -} - -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); - } - }); -} - -/**@internal */ -export function forEachTopLevelDeclaration(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; - } - } -} -/** @internal */ -export 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); -} - -/**@internal */ -export 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 forEachTopLevelDeclarationInBindingName(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}`); - } -} - -/**@internal */ -export 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; - } -} - -/**@internal */ -export interface ReadonlySymbolSet { - size(): number; - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -class SymbolSet implements ReadonlySymbolSet { - private map = new Map(); - 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(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; - } -} \ No newline at end of file diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts index 0f124c4d84030..ab7fa73a68afc 100644 --- a/src/services/moveToFileAndNewFile.ts +++ b/src/services/moveToFileAndNewFile.ts @@ -1,10 +1,10 @@ -import { cast, concatenate, contains, emptyArray, find, flatMap, GetCanonicalFileName, last,length,mapDefined,tryCast } from "../compiler/core"; +import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, flatMap, GetCanonicalFileName, getRangesWhere, last,length,mapDefined,some,tryCast } from "../compiler/core"; import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; -import { AnyImportOrRequireStatement, BindingElement, BindingName, CallExpression, DeclarationStatement, Expression, ExternalModuleReference, Identifier, ImportDeclaration, ImportEqualsDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, NamedImportBindings, Node, NodeFlags, Program, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TypeChecker, TypeNode, VariableDeclaration, VariableStatement } from "../compiler/types"; -import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, createModuleSpecifierResolutionHost, Debug, escapeLeadingUnderscores, factory,FindAllReferences,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBindingElement, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, skipAlias, symbolNameNoDefault, textChanges } from "./_namespaces/ts"; -import { forEachTopLevelDeclaration, isNonVariableTopLevelDeclaration, NonVariableTopLevelDeclaration, ReadonlySymbolSet,StatementRange, TopLevelDeclaration, TopLevelVariableDeclaration } from "./createNewFilename"; +import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; +import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, copyEntries, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachEntry,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSymbolId,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; +/**@internal */ export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { const cfg = program.getCompilerOptions().configFile; if (!cfg) return; @@ -20,12 +20,14 @@ export function addNewFileToTsconfig(program: Program, changes: textChanges.Chan } } +/**@internal */ export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { for (const { first, afterLast } of moved) { changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); } } +/**@internal */ export 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; @@ -33,6 +35,7 @@ export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Sta } } +/**@internal */ export function updateImportsInOtherFiles( changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newFilename: string, quotePreference: QuotePreference ): void { @@ -63,6 +66,7 @@ export function updateImportsInOtherFiles( } } +/**@internal */ export function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { switch (node.kind) { case SyntaxKind.ImportDeclaration: @@ -77,6 +81,7 @@ export function getNamespaceLikeImport(node: SupportedImport): Identifier | unde } } +/**@internal */ export function updateNamespaceLikeImport( changes: textChanges.ChangeTracker, sourceFile: SourceFile, @@ -107,7 +112,7 @@ export function updateNamespaceLikeImport( } } - function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { +function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string, quotePreference: QuotePreference): Node { const newNamespaceId = factory.createIdentifier(newNamespaceName); const newModuleString = makeStringLiteral(newModuleSpecifier, quotePreference); switch (node.kind) { @@ -130,12 +135,14 @@ function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); } +/**@internal */ export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression : i.initializer.arguments[0]); } +/**@internal */ export function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { if (isImportDeclaration(statement)) { if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); @@ -158,11 +165,14 @@ type SupportedImport = | ImportDeclaration & { moduleSpecifier: StringLiteralLike } | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } | VariableDeclaration & { initializer: RequireOrImportCall }; + +/**@internal */ export type SupportedImportStatement = | ImportDeclaration | ImportEqualsDeclaration | VariableStatement; +/**@internal */ export function createOldFileImportsFromNewFile( sourceFile: SourceFile, newFileNeedExport: ReadonlySymbolSet, @@ -185,6 +195,7 @@ export function createOldFileImportsFromNewFile( return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); } +/**@internal */ export function makeImportOrRequire( sourceFile: SourceFile, defaultImport: Identifier | undefined, @@ -215,6 +226,7 @@ function makeVariableStatement(name: BindingName, type: TypeNode | undefined, in return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); } +/**@internal */ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { return flatMap(toMove, statement => { if (isTopLevelDeclarationStatement(statement) && @@ -227,6 +239,7 @@ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], }); } +/**@internal */ export 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)); @@ -235,6 +248,7 @@ export function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStat getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); } +/**@internal */ export function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { switch (importDecl.kind) { case SyntaxKind.ImportDeclaration: @@ -282,6 +296,7 @@ function deleteUnusedImportsInDeclaration(sourceFile: SourceFile, importDecl: Im } } } + function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDecl: VariableDeclaration, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean) { const { name } = varDecl; switch (name.kind) { @@ -384,6 +399,7 @@ function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonl } } +/**@internal */ export function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { switch (i.kind) { case SyntaxKind.ImportDeclaration: { @@ -430,10 +446,12 @@ function filterBindingName(name: BindingName, keep: (name: Identifier) => boolea } } +/**@internal */ export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); } +/**@internal */ export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { switch (d.kind) { case SyntaxKind.VariableDeclaration: @@ -446,6 +464,7 @@ export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLeve } } +/**@internal */ export function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { if (isExported(sourceFile, decl, useEs6Exports, name)) return; if (useEs6Exports) { @@ -456,3 +475,353 @@ export function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclara if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); } } + +//creating a new filename + +/**@internal */ +export interface ToMove { + readonly all: readonly Statement[]; + readonly ranges: readonly StatementRange[]; +} + +/**@internal */ +export interface StatementRange { + readonly first: Statement; + readonly afterLast: Statement | undefined; +} + +/**@internal */ +export interface ReadonlySymbolSet { + size(): number; + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +} + +/**@internal */ +export 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; +} +type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' +/**@internal */ +export type NonVariableTopLevelDeclaration = + | FunctionDeclaration + | ClassDeclaration + | EnumDeclaration + | TypeAliasDeclaration + | InterfaceDeclaration + | ModuleDeclaration + | TopLevelExpressionStatement + | ImportEqualsDeclaration; + +/**@internal */ +export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } + /**@internal */ +export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; + +/** @internal */ +export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { + const checker = program.getTypeChecker(); + const toMove = getStatementsToMove(context); + let usage; + if (toMove) { + 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; + return newFilename; + } + return ""; +} + +/**@internal */ +export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } + +/**@internal */ +export 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], + }; +} + +/**@internal */ +export 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; + } +} + +/** @internal */ +export 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; + } +} + +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"; +} + +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); + } + }); +} + +/**@internal */ +export function forEachTopLevelDeclaration(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; + } + } +} +/** @internal */ +export 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); +} + +/**@internal */ +export 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 forEachTopLevelDeclarationInBindingName(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}`); + } +} + +/**@internal */ +export 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; + } +} + +/**@internal */ +export interface ReadonlySymbolSet { + size(): number; + has(symbol: Symbol): boolean; + forEach(cb: (symbol: Symbol) => void): void; + forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +} + +class SymbolSet implements ReadonlySymbolSet { + private map = new Map(); + 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(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; + } +} diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 8eb3d6c39a117..f1cb44a5a965b 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -29,8 +29,7 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { getStatementsToMove, getUsageInfo, isTopLevelDeclaration, ReadonlySymbolSet, ToMove, UsageInfo } from "../createNewFilename"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, updateImportsInOtherFiles } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 344e3f71f0897..cfd1ba19ba1ef 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -27,8 +27,7 @@ import { UserPreferences, } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -import { createNewFilename, getStatementsToMove, getUsageInfo, isTopLevelDeclaration,ReadonlySymbolSet, ToMove, UsageInfo } from "../createNewFilename"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getTopLevelDeclarationStatement, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, SupportedImportStatement, updateImportsInOtherFiles } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createNewFilename, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, SupportedImportStatement, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; const refactorName = "Move to a new file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); diff --git a/src/services/services.ts b/src/services/services.ts index 613b4bf1ba8b4..760952c3ffb97 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -321,7 +321,7 @@ import * as NavigateTo from "./_namespaces/ts.NavigateTo"; import * as NavigationBar from "./_namespaces/ts.NavigationBar"; import * as classifier from "./classifier"; import * as classifier2020 from "./classifier2020"; -import { createNewFilename } from "./createNewFilename"; +import { createNewFilename } from "./moveToFileAndNewFile"; /** The version of the language service API */ export const servicesVersion = "0.8"; From f98a45772b0488bb3aecafb7911db786bf0626f3 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 30 Mar 2023 18:01:18 -0700 Subject: [PATCH 16/42] removing `changeExistingFile` from textChanges --- src/services/refactors/moveToFile.ts | 4 +- src/services/refactors/moveToNewFile.ts | 2 +- src/services/textChanges.ts | 60 ++++++------------------- 3 files changed, 17 insertions(+), 49 deletions(-) diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index f1cb44a5a965b..7d741a4d77dcb 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -70,14 +70,14 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string const usage = getUsageInfo(oldFile, toMove.all, checker); //creating a new file if (!host.fileExists(newFile)) { - changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); + changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false), /*isNewFile*/ true); addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); } else { const sourceFile = program.getSourceFile(newFile); if (sourceFile) { const newFileImportAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); - changes.addStatementsToNewFile(oldFile, program.getSourceFile(newFile), getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder)); + changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder), /*isNewFile*/ false); } } } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index cfd1ba19ba1ef..e2be0628e3bfb 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -66,7 +66,7 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes 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)); + changes.addToNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences), /*isNewFile*/ true); addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 92934b114e8f2..c860af8a2c9c9 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -480,10 +480,9 @@ 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 readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], isNewFile: boolean }[] = []; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; - private readonly changeExistingFile: { readonly oldFile: SourceFile, readonly newFile: SourceFile | undefined, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; public static fromContext(context: TextChangesContext): ChangeTracker { return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext); @@ -1128,25 +1127,23 @@ export class ChangeTracker { public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); - let changes; - if (this.changeExistingFile && this.changes) { - for (const { oldFile } of this.changeExistingFile) { - changes = changesToText.getTextChangesFromChangesForMoveToExistingFile(this.changes, oldFile, this.newLineCharacter, this.formatContext, validate); - return changes; + let changes: FileTextChanges[] = []; + for (const { oldFile, fileName, statements, isNewFile } of this.newFiles) { + if (isNewFile) { + changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); + changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } + else { + changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate, oldFile); + } + return changes; } 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)); - } return changes; } - public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { - this.newFiles.push({ oldFile, fileName, statements }); - } - public addStatementsToNewFile(oldFile: SourceFile, newFile: SourceFile | undefined, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { - this.changeExistingFile.push({ oldFile, newFile, statements }); + public addToNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], isNewFile: boolean): void { + this.newFiles.push({ oldFile, fileName, statements, isNewFile }); } } @@ -1224,7 +1221,7 @@ export function getNewFileText(statements: readonly Statement[], scriptKind: Scr } namespace changesToText { - export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { + export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined, oldFile?: SourceFile): FileTextChanges[] { return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => { const sourceFile = changesInFile[0].sourceFile; // order changes by start position @@ -1238,36 +1235,7 @@ 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; - } - - return createTextChange(span, newText); - }); - - return textChanges.length > 0 ? { fileName: sourceFile.fileName, textChanges } : undefined; - }); - } - - export function getTextChangesFromChangesForMoveToExistingFile(changes: readonly Change[], oldFile: SourceFile, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { - return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => { - // order changes by start position - // If the start position is the same, put the shorter range first, since an empty range (x, x) may precede (x, y) but not vice-versa. - const normalized = stableSort(changesInFile, (a, b) => (a.range.pos - b.range.pos) || (a.range.end - b.range.end)); - // verify that change intervals do not overlap, except possibly at end points. - for (let i = 0; i < normalized.length - 1; i++) { - Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos, "Changes overlap", () => - `${JSON.stringify(normalized[i].range)} and ${JSON.stringify(normalized[i + 1].range)}`); - } - - const sourceFile = changesInFile[0].sourceFile; - const textChanges = mapDefined(normalized, c => { - const span = createTextSpanFromRange(c.range); - const newText = computeNewText(c, oldFile, newLineCharacter, formatContext, validate); - + const newText = (oldFile) ? computeNewText(c, oldFile, newLineCharacter, formatContext, validate): computeNewText(c, sourceFile, newLineCharacter, formatContext, validate); // Filter out redundant changes. if (span.length === newText.length && stringContainsAt(sourceFile.text, newText, span.start)) { return undefined; From eec29350e064f71faab9048fa1f14fcb2cf0d3f5 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 5 Apr 2023 13:56:12 -0700 Subject: [PATCH 17/42] fix for an 'allowImportingTsExtensions' bug --- src/compiler/moduleSpecifiers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 7b3ab92f60b04..de55dad938267 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -1044,7 +1044,10 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie return fileName; } - if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) { + if (allowedEndings[0] === ModuleSpecifierEnding.TsExtension && fileExtensionIsOneOf(fileName, [Extension.Mts, Extension.Cts])) { + 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.")) { From 9fd4778fd5c37f0c181d42f546ffa210703fab10 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 5 Apr 2023 14:09:13 -0700 Subject: [PATCH 18/42] cleanup, fixes after removing `changeExistingFile` --- src/services/refactors/moveToFile.ts | 30 ++++++++++++++++--------- src/services/refactors/moveToNewFile.ts | 2 +- src/services/textChanges.ts | 24 +++++++------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 7d741a4d77dcb..320282dc8b509 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -6,7 +6,6 @@ import { Debug, Diagnostics, emptyArray, - getBaseFileName, getLocaleSpecificMessage, getQuotePreference, hasSyntacticModifier, @@ -29,7 +28,7 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; @@ -70,14 +69,14 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string const usage = getUsageInfo(oldFile, toMove.all, checker); //creating a new file if (!host.fileExists(newFile)) { - changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false), /*isNewFile*/ true); + changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); } else { const sourceFile = program.getSourceFile(newFile); if (sourceFile) { const newFileImportAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); - changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder), /*isNewFile*/ false); + getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder); } } } @@ -103,7 +102,7 @@ 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, importAdder); + const imports = getNewFileImportsAndAddExportInOldFile(oldFile, newFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); if (newFileExists) { const sourceFile = program.getSourceFile(newFile); @@ -135,6 +134,7 @@ function getNewStatementsAndRemoveFromOldFile( function getNewFileImportsAndAddExportInOldFile( oldFile: SourceFile, + newFile: string, importsToCopy: ReadonlySymbolSet, newFileImportsFromOldFile: ReadonlySymbolSet, changes: textChanges.ChangeTracker, @@ -146,13 +146,21 @@ function getNewFileImportsAndAddExportInOldFile( newFileImportAdder?: codefix.ImportAdder, ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; - for (const oldStatement of oldFile.statements) { - forEachImportInStatement(oldStatement, i => { - append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + if (newFileImportAdder) { + importsToCopy.forEach(symbol => { + newFileImportAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); }); } + else { + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); + } + } //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. + const newFileSourceFile = program.getSourceFile(newFile); let oldFileDefault: Identifier | undefined; const oldFileNamedImports: string[] = []; const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. @@ -182,8 +190,8 @@ function getNewFileImportsAndAddExportInOldFile( } } }); - - append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, getBaseFileName(oldFile.fileName), program, host, useEsModuleSyntax, quotePreference)); - return copiedOldImports; + return (newFileSourceFile) + ? append(copiedOldImports, makeImportOrRequire(newFileSourceFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)) + : append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)); } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index e2be0628e3bfb..8848d0fda2941 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -66,7 +66,7 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes const newFilename = createNewFilename(oldFile, program, context, host); // If previous file was global, this is easy. - changes.addToNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences), /*isNewFile*/ true); + changes.addToNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences)); addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index c860af8a2c9c9..d4f21e2f24ad7 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -480,7 +480,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)[], isNewFile: boolean }[] = []; + private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; @@ -1127,23 +1127,15 @@ export class ChangeTracker { public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); - let changes: FileTextChanges[] = []; - for (const { oldFile, fileName, statements, isNewFile } of this.newFiles) { - if (isNewFile) { - changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); - changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); - } - else { - changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate, oldFile); - } - return changes; + 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)); } - changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); return changes; } - public addToNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], isNewFile: boolean): void { - this.newFiles.push({ oldFile, fileName, statements, isNewFile }); + public addToNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { + this.newFiles.push({ oldFile, fileName, statements }); } } @@ -1221,7 +1213,7 @@ export function getNewFileText(statements: readonly Statement[], scriptKind: Scr } namespace changesToText { - export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined, oldFile?: SourceFile): FileTextChanges[] { + export function getTextChangesFromChanges(changes: readonly Change[], newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): FileTextChanges[] { return mapDefined(group(changes, c => c.sourceFile.path), changesInFile => { const sourceFile = changesInFile[0].sourceFile; // order changes by start position @@ -1235,7 +1227,7 @@ namespace changesToText { const textChanges = mapDefined(normalized, c => { const span = createTextSpanFromRange(c.range); - const newText = (oldFile) ? computeNewText(c, oldFile, newLineCharacter, formatContext, validate): computeNewText(c, sourceFile, newLineCharacter, formatContext, validate); + 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; From b1cf74ce1bbf035ad5c2d8371bde67719592b80b Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 5 Apr 2023 14:11:47 -0700 Subject: [PATCH 19/42] fixi testcases & add getSynthesizedDeepClone --- src/services/moveToFileAndNewFile.ts | 8 +-- .../cases/fourslash/moveToFile10_ctsTomts.ts | 51 ++++++++++++++++++ tests/cases/fourslash/moveToFile2.ts | 1 + tests/cases/fourslash/moveToFile5.ts | 5 ++ tests/cases/fourslash/moveToFile6.ts | 5 ++ tests/cases/fourslash/moveToFile7_newFile.ts | 5 ++ .../moveToFile8_multipleStatements.ts | 25 +++++---- .../moveToFile9_differentDirectories.ts | 52 +++++++++++++++++++ 8 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 tests/cases/fourslash/moveToFile10_ctsTomts.ts create mode 100644 tests/cases/fourslash/moveToFile9_differentDirectories.ts diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts index ab7fa73a68afc..9a0b5c8d46465 100644 --- a/src/services/moveToFileAndNewFile.ts +++ b/src/services/moveToFileAndNewFile.ts @@ -2,7 +2,7 @@ import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; -import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, copyEntries, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachEntry,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSymbolId,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; +import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, copyEntries, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachEntry,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSymbolId,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; /**@internal */ export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { @@ -232,10 +232,10 @@ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], if (isTopLevelDeclarationStatement(statement) && !isExported(sourceFile, statement, useEs6Exports) && forEachTopLevelDeclaration(statement, d => needExport.has(Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol)))) { - const exports = addExport(statement, useEs6Exports); + const exports = addExport(getSynthesizedDeepClone(statement), useEs6Exports); if (exports) return exports; } - return statement; + return getSynthesizedDeepClone(statement); }); } @@ -408,7 +408,7 @@ export function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralL 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) + ? factory.createImportDeclaration(/*modifiers*/ undefined, factory.createImportClause(clause.isTypeOnly, defaultImport, namedBindings), getSynthesizedDeepClone(moduleSpecifier), /*assertClause*/ undefined) : undefined; } case SyntaxKind.ImportEqualsDeclaration: diff --git a/tests/cases/fourslash/moveToFile10_ctsTomts.ts b/tests/cases/fourslash/moveToFile10_ctsTomts.ts new file mode 100644 index 0000000000000..464fa2d316e8d --- /dev/null +++ b/tests/cases/fourslash/moveToFile10_ctsTomts.ts @@ -0,0 +1,51 @@ +/// + +// @module: nodenext +// @allowImportingTsExtensions: true +// @noEmit: true + +//@Filename: /src/dir2/bar.mts +////import './blah.ts'; +////import './blah2.ts'; +////const a = 2; +////a; + +// @Filename: /src/dir1/a.cts +////// header comment +//// +////import { a, b, alreadyUnused } from './other.cts'; +////const p = 0; +////[|const y = p + b;|] +////a; y; + +// @Filename: /src/dir1/other.cts +////export const b = 1; +////export const a = 2; +////export const alreadyUnused = "unused"; + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.cts": +`// header comment + +import { y } from '../dir2/bar.mts'; +import { a, alreadyUnused } from './other.cts'; +export const p = 0; +a; y;`, + + "/src/dir2/bar.mts": +`import { p } from '../dir1/a.cts'; +import { b } from '../dir1/other.cts'; +import './blah.ts'; +import './blah2.ts'; +const a = 2; +a; +export const y = p + b; +`, + }, + newFile: "/src/dir2/bar.mts", + + preferences: { + quotePreference: "single", + } +}); diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile2.ts index 26508373e9dcc..5333076d88e4f 100644 --- a/tests/cases/fourslash/moveToFile2.ts +++ b/tests/cases/fourslash/moveToFile2.ts @@ -1,6 +1,7 @@ /// +// @verbatimModuleSyntax: true // @Filename: /bar.ts //// const q = 0; diff --git a/tests/cases/fourslash/moveToFile5.ts b/tests/cases/fourslash/moveToFile5.ts index d8279abef4c40..d9ddda759760f 100644 --- a/tests/cases/fourslash/moveToFile5.ts +++ b/tests/cases/fourslash/moveToFile5.ts @@ -13,6 +13,11 @@ ////const p = 0; ////[|const y: Date = p + b;|] +// @Filename: /other.ts +////export const b = 2; +////export const a = 1; +////export const alreadyUnused = "unused"; + verify.moveToFile({ newFileContents: { "/a.ts": diff --git a/tests/cases/fourslash/moveToFile6.ts b/tests/cases/fourslash/moveToFile6.ts index ef4d14a665261..be5af498ba864 100644 --- a/tests/cases/fourslash/moveToFile6.ts +++ b/tests/cases/fourslash/moveToFile6.ts @@ -15,6 +15,11 @@ ////[|const y: Date = p + b;|] ////a; y; +// @Filename: /other.ts +////export const b = 1; +////export const a = 2; +////export const alreadyUnused = "unused"; + verify.moveToFile({ newFileContents: { "/a.ts": diff --git a/tests/cases/fourslash/moveToFile7_newFile.ts b/tests/cases/fourslash/moveToFile7_newFile.ts index bd25cc0d5c236..56afa1a039d11 100644 --- a/tests/cases/fourslash/moveToFile7_newFile.ts +++ b/tests/cases/fourslash/moveToFile7_newFile.ts @@ -9,6 +9,11 @@ ////[|const y: Date = p + b;|] ////a; y; +// @Filename: /other.ts +////export const b = 1; +////export const a = 2; +////export const alreadyUnused = "unused"; + verify.moveToFile({ newFileContents: { "/a.ts": diff --git a/tests/cases/fourslash/moveToFile8_multipleStatements.ts b/tests/cases/fourslash/moveToFile8_multipleStatements.ts index 35a3868f8a95a..ce9691577c234 100644 --- a/tests/cases/fourslash/moveToFile8_multipleStatements.ts +++ b/tests/cases/fourslash/moveToFile8_multipleStatements.ts @@ -12,16 +12,21 @@ ////import './foo'; ////import { a, b, alreadyUnused } from './other'; ////const p = 0; -////[|const x = 0; +////[|const q = 0; ////const t = 2; ////function f() {} ////class C {} ////enum E {} -////namespace N { export const x = 0; } +////namespace N { export const h = 0; } ////type T = number; ////interface I {}|] ////t; +// @Filename: /other.ts +////export const b = 1; +////export const a = 2; +////export const alreadyUnused = "unused"; + verify.moveToFile({ newFileContents: { "/a.ts": @@ -38,14 +43,14 @@ t;`, import './blah2'; const a = 2; a; -const x = 0; - export const t = 2; - function f() { } - class C { } - enum E { } - namespace N { export const x = 0; } - type T = number; - interface I { } +const q = 0; +export const t = 2; +function f() { } +class C { } +enum E { } +namespace N { export const h = 0; } +type T = number; +interface I { } `, }, newFile: "/bar.ts", diff --git a/tests/cases/fourslash/moveToFile9_differentDirectories.ts b/tests/cases/fourslash/moveToFile9_differentDirectories.ts new file mode 100644 index 0000000000000..06181e2a51853 --- /dev/null +++ b/tests/cases/fourslash/moveToFile9_differentDirectories.ts @@ -0,0 +1,52 @@ +/// + +//@moduleResolution: bundler +//@module: esnext + +//@Filename: /src/dir2/bar.ts +////import './blah'; +////import './blah2'; +////const a = 2; +////a; + +// @Filename: /src/dir1/a.ts +////// header comment +//// +////import './foo'; +////import { a, b, alreadyUnused } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] +////a; y; + +// @Filename: /src/dir1/other.ts +////export const b = 1; +////export const a = 2; +////export const alreadyUnused = "unused"; + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.ts": +`// header comment + +import { y } from '../dir2/bar'; +import './foo'; +import { a, alreadyUnused } from './other'; +export const p = 0; +a; y;`, + + "/src/dir2/bar.ts": +`import { p } from '../dir1/a'; +import { b } from '../dir1/other'; +import './blah'; +import './blah2'; +const a = 2; +a; +export const y: Date = p + b; +`, + }, + newFile: "/src/dir2/bar.ts", + + preferences: { + quotePreference: "single", + } +}); From 53c5fa26e095277aa6791e1d90ebc8d2bba422c0 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 5 Apr 2023 14:36:15 -0700 Subject: [PATCH 20/42] test needs to be fixed --- tests/cases/fourslash/moveToFile2.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile2.ts index 5333076d88e4f..e37c122293868 100644 --- a/tests/cases/fourslash/moveToFile2.ts +++ b/tests/cases/fourslash/moveToFile2.ts @@ -1,8 +1,6 @@ /// -// @verbatimModuleSyntax: true - // @Filename: /bar.ts //// const q = 0; @@ -20,7 +18,7 @@ verify.moveToFile({ newFileContents: { "/a.ts":"", "/bar.ts": -`import { type A } from "./other"; +`import { A } from "./other"; const q = 0; function f(a: A) { } From 93bca9cbd3b12afa8a74a88a153ae3ab034346ae Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 5 Apr 2023 17:11:03 -0700 Subject: [PATCH 21/42] Ensure cloned NodeArrays have a text range set --- src/services/utilities.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 7b29fd02dfb5e..8c483722f873f 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3130,7 +3130,11 @@ export function getSynthesizedDeepClones(nodes: NodeArray, in export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia?: boolean): NodeArray | undefined; /** @internal */ export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia = true): NodeArray | 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 nodes; } /** @internal */ From 7cb66ddca975d6e3fa6fe29f498dd8a2ce093ab7 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 5 Apr 2023 16:02:39 -0700 Subject: [PATCH 22/42] fixes for suggesting a list of files --- src/services/services.ts | 13 +- .../getMoveToRefactoringFileSuggestions.ts | 97 ++++------- .../skips-lib.d.ts-files.js | 118 ++++++++++++++ ...ggests-only-.js-file-for-a-.js-filepath.js | 129 +++++++++++++++ ...ggests-only-.ts-file-for-a-.ts-filepath.js | 151 ++++++++++++++++++ ...excluding-node_modules-within-a-project.js | 1 - 6 files changed, 432 insertions(+), 77 deletions(-) create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js create mode 100644 tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js diff --git a/src/services/services.ts b/src/services/services.ts index 760952c3ffb97..61cae14d55ddb 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -64,9 +64,8 @@ import { EntityName, equateValues, ExportDeclaration, + Extension, extensionFromPath, - extensionIsTS, - fileExtensionIs, FileReference, FileTextChanges, filter, @@ -87,6 +86,7 @@ import { getAdjustedRenameLocation, getAllSuperTypeNodes, getAssignmentDeclarationKind, + getBaseFileName, GetCompletionsAtPositionOptions, getContainerNode, getDefaultLibFileName, @@ -121,7 +121,6 @@ import { hasStaticModifier, hasSyntacticModifier, hasTabstop, - hasTSFileExtension, HostCancellationToken, hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, @@ -276,6 +275,7 @@ import { SourceFile, SourceFileLike, SourceMapSource, + startsWith, Statement, stringContains, StringLiteral, @@ -2933,10 +2933,11 @@ export function createLanguageService( const files: string[] = []; if (allFiles) { for (const file of allFiles) { - if (extensionIsTS(extension) && hasTSFileExtension(file)) { - files.push(file); + const fileExtension = extensionFromPath(file); + if (sourceFile === getValidSourceFile(file) || extension === Extension.Ts && fileExtension === Extension.Dts || extension === Extension.Dts && startsWith(getBaseFileName(file), "lib.") && fileExtension === Extension.Dts) { + continue; } - else if (fileExtensionIs(file, extension)){ + if (extension === fileExtension) { files.push(file); } } diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts index ef188d0bfe809..bc05c8b41c035 100644 --- a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -47,71 +47,7 @@ import { value1 } from "../node_modules/.cache/someFile.d.ts";` }); baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, excluding node_modules within a project", session); }); - it("works for suggesting a list of files, for a file belonging to multiple projects", () => { - const file1: File = { - path: "/blah/file1.ts", - content: `class CC { }` - }; - const file2: File = { path: "/blah/file2.ts", content: "" }; - const configFile1: File = { - path: "/blah/tsconfig.json", - content: `{ "files": ["./file1.ts", "./file2.ts"] }`, - }; - const file3: File = { path: "/blah-tests/file3.ts", content: `import { value1 } from "../blah/file1.ts";` }; - const file4: File = { path: "/blah-tests/file4.ts", content: "" }; - const configFile2: File = { - path: "/blah-tests/tsconfig.json", - content: `{ "files": ["./file3.ts", "./file4.ts"] }`, - }; - const host = createServerHost([file1, file2, file3, file4, configFile1, configFile2]); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file1], session); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file1.path, line: 1, offset: 7 } - }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for a file belonging to multiple projects", session); - }); - it("works for suggesting a list of files, for an inferred project for a .ts file", () => { - const file1: File = { path: "/file1.ts", content: 'import {} from "./file.ts";' }; - const file2: File = { - path: "/file2.ts", - content: `interface ka { - name: string; - } - ` - }; - const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./file1.ts", "./file.ts"] }) }; - - const host = createServerHost([file1, file2, tsconfig]); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file2], session); - - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file2.path, line: 1, offset: 11 } - }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project for a .ts file", session); - }); - it("works for suggesting a list of files, for an inferred project for a .js file", () => { - const file1: File = { path: "/file1.js", content: 'import {} from "./file.js";' }; - const file2: File = { - path: "/file2.js", - content: `class C {}` - }; - const tsconfig: File = { path: "/jsconfig.json", content: JSON.stringify({ files: ["./file1.js", "./file.js"] }) }; - - const host = createServerHost([file1, file2, tsconfig]); - const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); - openFilesForSession([file2], session); - - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, - arguments: { file: file2.path, line: 1, offset: 7 } - }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works for suggesting a list of files, for an inferred project for a .js file", session); - }); - it("works with different .ts extensions", () => { + it("suggests only .ts file for a .ts filepath", () => { const file1: File = { path: "/file1.ts", content: `interface ka { @@ -124,9 +60,10 @@ import { value1 } from "../node_modules/.cache/someFile.d.ts";` const file4: File = { path: "/file4.cts", content: "" }; const file5: File = { path: "/file5.js", content: "" }; const file6: File = { path: "/file6.d.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"] }) }; + 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, tsconfig]); + const host = createServerHost([file1, file2, file3, file4, file5, file6, file7, tsconfig]); const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); openFilesForSession([file1], session); @@ -134,9 +71,9 @@ import { value1 } from "../node_modules/.cache/someFile.d.ts";` command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, arguments: { file: file1.path, line: 1, offset: 11 } }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different .ts extensions", session); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "suggests only .ts file for a .ts filepath", session); }); - it("works with different extensions", () => { + it("suggests only .js file for a .js filepath", () => { const file1: File = { path: "/file1.js", content: `class C {}` @@ -155,6 +92,26 @@ import { value1 } from "../node_modules/.cache/someFile.d.ts";` command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, arguments: { file: file1.path, line: 1, offset: 7 } }); - baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "works with different extensions", session); + 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({ + command: ts.server.protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, + arguments: { file: file1.path, line: 1, offset: 7 } + }); + baselineTsserverLogs("getMoveToRefactoringFileSuggestions", "skips lib.d.ts files", session); }); }); \ No newline at end of file diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js new file mode 100644 index 0000000000000..3a28e6b0a1983 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js @@ -0,0 +1,118 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.d.ts] +class C {} + +//// [/a/lib.d.ts] + + +//// [/a/file3.d.ts] + + +//// [/a/lib.es6.d.ts] + + +//// [/tsconfig.json] +{"files":["./file1.d.ts","./a/lib.d.ts","./a/file3.d.ts","/a/lib.es6.d.ts"]} + + +Info 1 [00:00:16.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.d.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:17.000] Search path: / +Info 3 [00:00:18.000] For info: /file1.d.ts :: Config file name: /tsconfig.json +Info 4 [00:00:19.000] Creating configuration project /tsconfig.json +Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:21.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.d.ts", + "/a/lib.d.ts", + "/a/file3.d.ts", + "/a/lib.es6.d.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /a/lib.d.ts 500 undefined WatchType: Closed Script info +Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/file3.d.ts 500 undefined WatchType: Closed Script info +Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /a/lib.es6.d.ts 500 undefined WatchType: Closed Script info +Info 10 [00:00:25.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 11 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 12 [00:00:27.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 13 [00:00:28.000] Project '/tsconfig.json' (Configured) +Info 14 [00:00:29.000] Files (4) + /file1.d.ts SVC-1-0 "class C {}" + /a/lib.d.ts Text-1 "" + /a/file3.d.ts Text-1 "" + /a/lib.es6.d.ts Text-1 "" + + + file1.d.ts + Part of 'files' list in tsconfig.json + a/lib.d.ts + Part of 'files' list in tsconfig.json + a/file3.d.ts + Part of 'files' list in tsconfig.json + a/lib.es6.d.ts + Part of 'files' list in tsconfig.json + +Info 15 [00:00:30.000] ----------------------------------------------- +Info 16 [00:00:31.000] Project '/tsconfig.json' (Configured) +Info 16 [00:00:32.000] Files (4) + +Info 16 [00:00:33.000] ----------------------------------------------- +Info 16 [00:00:34.000] Open files: +Info 16 [00:00:35.000] FileName: /file1.d.ts ProjectRootPath: undefined +Info 16 [00:00:36.000] Projects: /tsconfig.json +Info 16 [00:00:37.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/a/lib.d.ts: *new* + {} +/a/file3.d.ts: *new* + {} +/a/lib.es6.d.ts: *new* + {} + +Before request + +Info 17 [00:00:38.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.d.ts", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info 18 [00:00:39.000] response: + { + "response": { + "newFilename": "/C.d.ts", + "files": [ + "/a/file3.d.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js new file mode 100644 index 0000000000000..6d5f7abb92a04 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js @@ -0,0 +1,129 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.js] +class C {} + +//// [/file2.js] + + +//// [/file3.mts] + + +//// [/file4.ts] + + +//// [/file5.js] + + +//// [/tsconfig.json] +{"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} + + +Info 1 [00:00:16.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.js" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:17.000] Search path: / +Info 3 [00:00:18.000] For info: /file1.js :: Config file name: /tsconfig.json +Info 4 [00:00:19.000] Creating configuration project /tsconfig.json +Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:21.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.js", + "/file2.js", + "/file3.mts", + "/file4.ts", + "/file5.js" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info +Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info +Info 10 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:26.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 12 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 13 [00:00:28.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 14 [00:00:29.000] Project '/tsconfig.json' (Configured) +Info 15 [00:00:30.000] Files (5) + /file1.js SVC-1-0 "class C {}" + /file2.js Text-1 "" + /file3.mts Text-1 "" + /file4.ts Text-1 "" + /file5.js Text-1 "" + + + file1.js + Part of 'files' list in tsconfig.json + file2.js + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.ts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + +Info 16 [00:00:31.000] ----------------------------------------------- +Info 17 [00:00:32.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:33.000] Files (5) + +Info 17 [00:00:34.000] ----------------------------------------------- +Info 17 [00:00:35.000] Open files: +Info 17 [00:00:36.000] FileName: /file1.js ProjectRootPath: undefined +Info 17 [00:00:37.000] Projects: /tsconfig.json +Info 17 [00:00:38.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.js: *new* + {} +/file3.mts: *new* + {} +/file4.ts: *new* + {} +/file5.js: *new* + {} + +Before request + +Info 18 [00:00:39.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.js", + "line": 1, + "offset": 7 + }, + "seq": 2, + "type": "request" + } +Info 19 [00:00:40.000] response: + { + "response": { + "newFilename": "/C.js", + "files": [ + "/file2.js", + "/file5.js" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js new file mode 100644 index 0000000000000..8e32072c58c41 --- /dev/null +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js @@ -0,0 +1,151 @@ +currentDirectory:: / useCaseSensitiveFileNames: false +Info 0 [00:00:19.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Before request +//// [/file1.ts] +interface ka { + name: string; + } + + +//// [/file2.tsx] + + +//// [/file3.mts] + + +//// [/file4.cts] + + +//// [/file5.js] + + +//// [/file6.d.ts] + + +//// [/file7.ts] + + +//// [/tsconfig.json] +{"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts","./file7.ts"]} + + +Info 1 [00:00:20.000] request: + { + "command": "open", + "arguments": { + "file": "/file1.ts" + }, + "seq": 1, + "type": "request" + } +Info 2 [00:00:21.000] Search path: / +Info 3 [00:00:22.000] For info: /file1.ts :: Config file name: /tsconfig.json +Info 4 [00:00:23.000] Creating configuration project /tsconfig.json +Info 5 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info 6 [00:00:25.000] Config: /tsconfig.json : { + "rootNames": [ + "/file1.ts", + "/file2.tsx", + "/file3.mts", + "/file4.cts", + "/file5.js", + "/file6.d.ts", + "/file7.ts" + ], + "options": { + "configFilePath": "/tsconfig.json" + } +} +Info 7 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info +Info 8 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info 9 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info +Info 10 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info 11 [00:00:30.000] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info +Info 12 [00:00:31.000] FileWatcher:: Added:: WatchInfo: /file7.ts 500 undefined WatchType: Closed Script info +Info 13 [00:00:32.000] Starting updateGraphWorker: Project: /tsconfig.json +Info 14 [00:00:33.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info 15 [00:00:34.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info 16 [00:00:35.000] Project '/tsconfig.json' (Configured) +Info 17 [00:00:36.000] Files (7) + /file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " + /file2.tsx Text-1 "" + /file3.mts Text-1 "" + /file4.cts Text-1 "" + /file5.js Text-1 "" + /file6.d.ts Text-1 "" + /file7.ts Text-1 "" + + + file1.ts + Part of 'files' list in tsconfig.json + file2.tsx + Part of 'files' list in tsconfig.json + file3.mts + Part of 'files' list in tsconfig.json + file4.cts + Part of 'files' list in tsconfig.json + file5.js + Part of 'files' list in tsconfig.json + file6.d.ts + Part of 'files' list in tsconfig.json + file7.ts + Part of 'files' list in tsconfig.json + +Info 18 [00:00:37.000] ----------------------------------------------- +Info 19 [00:00:38.000] Project '/tsconfig.json' (Configured) +Info 19 [00:00:39.000] Files (7) + +Info 19 [00:00:40.000] ----------------------------------------------- +Info 19 [00:00:41.000] Open files: +Info 19 [00:00:42.000] FileName: /file1.ts ProjectRootPath: undefined +Info 19 [00:00:43.000] Projects: /tsconfig.json +Info 19 [00:00:44.000] response: + { + "responseRequired": false + } +After request + +PolledWatches:: +/a/lib/lib.d.ts: *new* + {"pollingInterval":500} + +FsWatches:: +/tsconfig.json: *new* + {} +/file2.tsx: *new* + {} +/file3.mts: *new* + {} +/file4.cts: *new* + {} +/file5.js: *new* + {} +/file6.d.ts: *new* + {} +/file7.ts: *new* + {} + +Before request + +Info 20 [00:00:45.000] request: + { + "command": "getMoveToRefactoringFileSuggestions", + "arguments": { + "file": "/file1.ts", + "line": 1, + "offset": 11 + }, + "seq": 2, + "type": "request" + } +Info 21 [00:00:46.000] response: + { + "response": { + "newFilename": "/ka.ts", + "files": [ + "/file7.ts" + ] + }, + "responseRequired": true + } +After request diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js index d1c9edcfb3b5f..fbb83cef9af39 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -142,7 +142,6 @@ Info 26 [00:01:08.000] response: "response": { "newFilename": "/project/a/ka.ts", "files": [ - "/project/a/file1.ts", "/project/a/file4.ts", "/project/b/file2.ts", "/project/d/e/file3.ts" From 63d2a837ecb6517ece6bad467047849f77f8df36 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 5 Apr 2023 17:11:03 -0700 Subject: [PATCH 23/42] Ensure cloned NodeArrays have a text range set --- src/services/utilities.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 7b29fd02dfb5e..e96f1c603556e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3130,7 +3130,12 @@ export function getSynthesizedDeepClones(nodes: NodeArray, in export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia?: boolean): NodeArray | undefined; /** @internal */ export function getSynthesizedDeepClones(nodes: NodeArray | undefined, includeTrivia = true): NodeArray | 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 */ From a681bc994856bdde7bf78438422ab5327e520504 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 5 Apr 2023 16:02:39 -0700 Subject: [PATCH 24/42] Allow ImportAdder to insert imports into a new file Co-authored-by: Andrew Branch --- src/compiler/moduleSpecifiers.ts | 23 ++--- src/compiler/types.ts | 9 ++ src/compiler/utilities.ts | 32 +++---- .../codefixes/convertFunctionToEs6Class.ts | 4 +- src/services/codefixes/convertToEsModule.ts | 6 +- src/services/codefixes/fixAddMissingMember.ts | 8 +- .../codefixes/fixInvalidImportSyntax.ts | 4 +- .../fixNoPropertyAccessFromIndexSignature.ts | 4 +- src/services/codefixes/helpers.ts | 6 +- src/services/codefixes/importFixes.ts | 83 ++++++++++--------- src/services/codefixes/useDefaultImport.ts | 4 +- src/services/completions.ts | 12 +-- src/services/exportInfoMap.ts | 8 +- src/services/refactors/moveToNewFile.ts | 4 +- src/services/textChanges.ts | 44 +++++++--- src/services/utilities.ts | 53 +++++++++--- 16 files changed, 190 insertions(+), 114 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index de55dad938267..f9b996e253b31 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -31,6 +31,7 @@ import { flatten, forEach, forEachAncestorDirectory, + FutureSourceFile, getBaseFileName, GetCanonicalFileName, getConditions, @@ -126,7 +127,7 @@ interface Preferences { function getPreferences( { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, oldImportSpecifier?: string, ): Preferences { const preferredEnding = getPreferredEnding(); @@ -212,7 +213,7 @@ export function getModuleSpecifier( /** @internal */ export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, @@ -243,14 +244,14 @@ function getModuleSpecifierWorker( /** @internal */ export function tryGetModuleSpecifiersFromCache( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFilePath: Path, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] | undefined { return tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFile, + importingSourceFilePath, host, userPreferences, options)[0]; @@ -258,7 +259,7 @@ export function tryGetModuleSpecifiersFromCache( function tryGetModuleSpecifiersFromCacheWorker( moduleSymbol: Symbol, - importingSourceFile: SourceFile, + importingSourceFilePath: Path, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -269,7 +270,7 @@ function tryGetModuleSpecifiersFromCacheWorker( } const cache = host.getModuleSpecifierCache?.(); - const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); + const cached = cache?.get(importingSourceFilePath, moduleSourceFile.path, userPreferences, options); return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; } @@ -303,7 +304,7 @@ export function getModuleSpecifiersWithCacheInfo( moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -315,7 +316,7 @@ export function getModuleSpecifiersWithCacheInfo( // eslint-disable-next-line prefer-const let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFile, + importingSourceFile.path, host, userPreferences, options @@ -333,14 +334,14 @@ export function getModuleSpecifiersWithCacheInfo( function computeModuleSpecifiers( modulePaths: readonly ModulePath[], compilerOptions: CompilerOptions, - importingSourceFile: SourceFile, + importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] { const info = getInfo(importingSourceFile.path, host); const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); - const existingSpecifier = forEach(modulePaths, modulePath => forEach( + const existingSpecifier = importingSourceFile.kind && forEach(modulePaths, modulePath => forEach( host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; @@ -877,7 +878,7 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam return processEnding(shortest, allowedEndings, compilerOptions); } -function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { +function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 7e14ff22dede6..edbb47928c3c4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4220,6 +4220,15 @@ export interface SourceFileLike { getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } +/** @internal */ +export interface FutureSourceFile { + readonly kind?: undefined; + readonly fileName: string; + readonly path: Path; + readonly impliedNodeFormat?: ResolutionMode; + readonly commonJsModuleIndicator?: boolean; + readonly externalModuleIndicator?: boolean; +} /** @internal */ export interface RedirectInfo { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index bf657d5a15dba..65b5aa5be2e59 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -157,6 +157,7 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, + FutureSourceFile, GetAccessorDeclaration, getBaseFileName, GetCanonicalFileName, @@ -9160,7 +9161,7 @@ export function usesExtensionsOnImports({ imports }: SourceFile, hasExtension: ( } /** @internal */ -export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile): ModuleSpecifierEnding { +export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile | FutureSourceFile): ModuleSpecifierEnding { if (preference === "js" || resolutionMode === ModuleKind.ESNext) { // Extensions are explicitly requested or required. Now choose between .js and .ts. if (!shouldAllowImportingTsExtension(compilerOptions)) { @@ -9185,27 +9186,30 @@ export function getModuleSpecifierEndingPreference(preference: UserPreferences[" // accurately, and more importantly, literally nobody wants `Index` and its existence is a mystery. if (!shouldAllowImportingTsExtension(compilerOptions)) { // If .ts imports are not valid, we only need to see one .js import to go with that. - return usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; + return sourceFile.kind && usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } return inferPreference(); function inferPreference() { - let usesJsExtensions = false; - const specifiers = sourceFile.imports.length ? sourceFile.imports.map(i => i.text) : - isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0].text) : - emptyArray; - for (const specifier of specifiers) { - if (pathIsRelative(specifier)) { - if (hasTSFileExtension(specifier)) { - return ModuleSpecifierEnding.TsExtension; - } - if (hasJSFileExtension(specifier)) { - usesJsExtensions = true; + if (sourceFile.kind) { + let usesJsExtensions = false; + const specifiers = sourceFile.imports.length ? sourceFile.imports.map(i => i.text) : + isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0].text) : + emptyArray; + for (const specifier of specifiers) { + if (pathIsRelative(specifier)) { + if (hasTSFileExtension(specifier)) { + return ModuleSpecifierEnding.TsExtension; + } + if (hasJSFileExtension(specifier)) { + usesJsExtensions = true; + } } } + return usesJsExtensions ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } - return usesJsExtensions ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; + return ModuleSpecifierEnding.Minimal; } } diff --git a/src/services/codefixes/convertFunctionToEs6Class.ts b/src/services/codefixes/convertFunctionToEs6Class.ts index 5073d687a67ed..5c46bed9c57d8 100644 --- a/src/services/codefixes/convertFunctionToEs6Class.ts +++ b/src/services/codefixes/convertFunctionToEs6Class.ts @@ -21,7 +21,7 @@ import { FunctionExpression, getEmitScriptTarget, getNameOfDeclaration, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, idText, isAccessExpression, @@ -211,7 +211,7 @@ function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, po // f.x = expr if (isAccessExpression(memberDeclaration) && (isFunctionExpression(assignmentExpr) || isArrowFunction(assignmentExpr))) { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const name = tryGetPropertyName(memberDeclaration, compilerOptions, quotePreference); if (name) { createFunctionLikeExpressionMember(members, assignmentExpr, name); diff --git a/src/services/codefixes/convertToEsModule.ts b/src/services/codefixes/convertToEsModule.ts index b15a0521c1cb1..1f8b08d5e9b45 100644 --- a/src/services/codefixes/convertToEsModule.ts +++ b/src/services/codefixes/convertToEsModule.ts @@ -27,7 +27,7 @@ import { FunctionExpression, getEmitScriptTarget, getModeForUsageLocation, - getQuotePreference, + getQuotePreferenceFromFile, getResolvedModule, getSynthesizedDeepClone, getSynthesizedDeepClones, @@ -87,10 +87,10 @@ registerCodeFix({ getCodeActions(context) { const { sourceFile, program, preferences } = context; const changes = textChanges.ChangeTracker.with(context, changes => { - const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreference(sourceFile, preferences)); + const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreferenceFromFile(sourceFile, preferences)); if (moduleExportsChangedToDefault) { for (const importingFile of program.getSourceFiles()) { - fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences)); + fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreferenceFromFile(importingFile, preferences)); } } }); diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 3d46bc1511d2a..464d58653a748 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -33,7 +33,7 @@ import { getNodeId, getObjectFlags, getOrUpdate, - getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getTokenAtPosition, hasAbstractModifier, @@ -597,7 +597,7 @@ function addEnumMemberDeclaration(changes: textChanges.ChangeTracker, checker: T } function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) { - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); const functionDeclaration = info.kind === InfoKind.Function ? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) @@ -614,7 +614,7 @@ function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: Cod function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const checker = context.program.getTypeChecker(); const jsxAttributesNode = info.parentDeclaration.attributes; const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute); @@ -634,7 +634,7 @@ function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixCo function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const target = getEmitScriptTarget(context.program.getCompilerOptions()); const checker = context.program.getTypeChecker(); const props = map(info.properties, prop => { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index bae3f2d2c0f16..9a88b3b49fc64 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -8,7 +8,7 @@ import { findAncestor, getEmitModuleKind, getNamespaceDeclarationNode, - getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getTokenAtPosition, ImportDeclaration, @@ -39,7 +39,7 @@ function getCodeFixesForImportDeclaration(context: CodeFixContext, node: ImportD const variations: CodeFixAction[] = []; // import Bluebird from "bluebird"; - variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreference(sourceFile, context.preferences)))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, context.preferences)))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts index 7a35126032924..1e517583bf85b 100644 --- a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts +++ b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts @@ -2,7 +2,7 @@ import { cast, Diagnostics, factory, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, isPropertyAccessChain, isPropertyAccessExpression, @@ -37,7 +37,7 @@ registerCodeFix({ }); function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: PropertyAccessExpression, preferences: UserPreferences): void { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const argumentsExpression = factory.createStringLiteral(node.name.text, quotePreference === QuotePreference.Single); changes.replaceNode( sourceFile, diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index e47e4c99777a2..00a878b049de6 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -27,7 +27,7 @@ import { getModuleSpecifierResolverHost, getNameForExportedSymbol, getNameOfDeclaration, - getQuotePreference, + getQuotePreferenceFromFile, getSetAccessorValueParameter, getSynthesizedDeepClone, getTokenAtPosition, @@ -205,7 +205,7 @@ export function addNewNodeForMemberSymbol( const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); const optional = !!(symbol.flags & SymbolFlags.Optional); const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient; - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); switch (kind) { case SyntaxKind.PropertySignature: @@ -467,7 +467,7 @@ export function createSignatureDeclarationFromCallExpression( modifierFlags: ModifierFlags, contextNode: Node ): MethodDeclaration | FunctionDeclaration | MethodSignature { - const quotePreference = getQuotePreference(context.sourceFile, context.preferences); + const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); const tracker = getNoopSymbolTrackerWithResolver(context); const checker = context.program.getTypeChecker(); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 6e7f9916748b7..feeeceed0b749 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -31,6 +31,7 @@ import { flatMapIterator, forEachExternalModuleToImportFrom, formatting, + FutureSourceFile, getAllowSyntheticDefaultImports, getBaseFileName, getDefaultExportInfoWorker, @@ -46,11 +47,13 @@ import { getNodeId, getQuoteFromPreference, getQuotePreference, + getQuotePreferenceFromFile, getSourceFileOfNode, getSymbolId, getTokenAtPosition, getTypeKeywordOfTypeOnlyImport, getUniqueSymbolId, + hasJSFileExtension, hostGetCanonicalFileName, Identifier, ImportClause, @@ -61,7 +64,6 @@ import { ImportsNotUsedAsValues, insertImports, InternalSymbolName, - isExternalModule, isExternalModuleReference, isIdentifier, isIdentifierPart, @@ -208,7 +210,7 @@ interface AddToExistingState { readonly namedImports: Map; } -function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { +function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { const compilerOptions = program.getCompilerOptions(); // Namespace fixes don't conflict, so just build a list. const addToNamespace: FixUseNamespaceImport[] = []; @@ -232,7 +234,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions)); const checker = program.getTypeChecker(); const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); - const exportInfo = getAllExportInfoForSymbol(sourceFile, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); + const exportInfo = getAllExportInfoForSymbol(sourceFile.path, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); const useRequire = shouldUseRequire(sourceFile, program); const fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); if (fix) { @@ -346,14 +348,17 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu } function writeFixes(changeTracker: textChanges.ChangeTracker) { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = sourceFile.kind ? getQuotePreferenceFromFile(sourceFile, preferences) : getQuotePreference(preferences); for (const fix of addToNamespace) { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addNamespaceQualifier(changeTracker, sourceFile, fix); } for (const fix of importType) { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addImportType(changeTracker, sourceFile, fix, quotePreference); } addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports }) => { + Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); doAddExistingFix( changeTracker, sourceFile, @@ -509,14 +514,14 @@ export function getImportCompletionAction( if (exportMapKey) { // The new way: `exportMapKey` should be in the `data` of each auto-import completion entry and // sent back when asking for details. - exportInfos = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); + exportInfos = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified exportMapKey"); } else { // The old way, kept alive for super old editors that don't give us `data` back. exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name)) ? [getSingleExportInfoForSymbol(targetSymbol, symbolName, moduleSymbol, program, host)] - : getAllExportInfoForSymbol(sourceFile, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); + : getAllExportInfoForSymbol(sourceFile.path, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified symbol / moduleSymbol"); } @@ -545,7 +550,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences)); } -function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { +function getImportFixForSymbol(sourceFile: SourceFile | FutureSourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { const packageJsonImportFilter = createPackageJsonImportFilter(sourceFile, preferences, host); return getBestFix(getImportFixes(exportInfos, position, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences).fixes, sourceFile, program, packageJsonImportFilter, host); } @@ -554,10 +559,10 @@ function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAc return { description, changes, commands }; } -function getAllExportInfoForSymbol(importingFile: SourceFile, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { +function getAllExportInfoForSymbol(importingFilePath: Path, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { const getChecker = createGetChecker(program, host); - return getExportInfoMap(importingFile, host, program, preferences, cancellationToken) - .search(importingFile.path, preferCapitalized, name => name === symbolName, info => { + return getExportInfoMap(importingFilePath, host, program, preferences, cancellationToken) + .search(importingFilePath, preferCapitalized, name => name === symbolName, info => { if (skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson)) === symbol && info.some(i => i.moduleSymbol === moduleSymbol || i.symbol.parent === moduleSymbol)) { return info; } @@ -591,16 +596,16 @@ function getImportFixes( isValidTypeOnlyUseSite: boolean, useRequire: boolean, program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, host: LanguageServiceHost, preferences: UserPreferences, - importMap = createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), + importMap = sourceFile.kind && createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly ImportFixWithModuleSpecifier[] } { const checker = program.getTypeChecker(); - const existingImports = flatMap(exportInfos, importMap.getImportsForExportInfo); - const useNamespace = usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); - const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); + const existingImports = importMap && flatMap(exportInfos, importMap.getImportsForExportInfo); + const useNamespace = existingImports && usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); + const addToExisting = existingImports && tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); if (addToExisting) { // Don't bother providing an action to add a new import if we can add to an existing one. return { @@ -787,9 +792,9 @@ function createExistingImportMap(checker: TypeChecker, importingFile: SourceFile }; } -function shouldUseRequire(sourceFile: SourceFile, program: Program): boolean { +function shouldUseRequire(sourceFile: SourceFile | FutureSourceFile, program: Program): boolean { // 1. TypeScript files don't use require variable declarations - if (!isSourceFileJS(sourceFile)) { + if (sourceFile.kind && !isSourceFileJS(sourceFile) || !sourceFile.kind && !hasJSFileExtension(sourceFile.fileName)) { return false; } @@ -820,7 +825,7 @@ function createGetChecker(program: Program, host: LanguageServiceHost) { function getNewImportFixes( program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -829,14 +834,14 @@ function getNewImportFixes( preferences: UserPreferences, fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[] } { - const isJs = isSourceFileJS(sourceFile); + const isJs = sourceFile.kind ? isSourceFileJS(sourceFile) : hasJSFileExtension(sourceFile.fileName); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); const getChecker = createGetChecker(program, host); const moduleResolution = getEmitModuleResolutionKind(compilerOptions); const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(moduleResolution); const getModuleSpecifiers = fromCacheOnly - ? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) + ? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile.path, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) : (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences); let computedWithoutCacheCount = 0; @@ -892,9 +897,9 @@ function getNewImportFixes( function getFixesForAddImport( exportInfos: readonly SymbolExportInfo[], - existingImports: readonly FixAddToExistingImportInfo[], + existingImports: readonly FixAddToExistingImportInfo[] | undefined, program: Program, - sourceFile: SourceFile, + sourceFile: SourceFile | FutureSourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -958,7 +963,7 @@ function sortFixInfo(fixes: readonly (FixInfo & { fix: ImportFixWithModuleSpecif compareModuleSpecifiers(a.fix, b.fix, sourceFile, program, packageJsonImportFilter.allowsImportingSpecifier, _toPath)); } -function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { +function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile | FutureSourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { if (!some(fixes)) return; // These will always be placed first if available, and are better than other kinds if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) { @@ -982,7 +987,7 @@ function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: function compareModuleSpecifiers( a: ImportFixWithModuleSpecifier, b: ImportFixWithModuleSpecifier, - importingFile: SourceFile, + importingFile: SourceFile | FutureSourceFile, program: Program, allowsImportingSpecifier: (specifier: string) => boolean, toPath: (fileName: string) => Path, @@ -1003,7 +1008,7 @@ function compareModuleSpecifiers( // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index" (we don't even try to consider // this if we're in a resolution mode where you can't drop trailing "/index" from paths). -function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { +function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { if (fix.isReExport && fix.exportInfo?.moduleFileName && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node10 && @@ -1019,7 +1024,7 @@ function isIndexFileName(fileName: string) { return getBaseFileName(fileName, [".js", ".jsx", ".d.ts", ".ts", ".tsx"], /*ignoreCase*/ true) === "index"; } -function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile, program: Program): Comparison { +function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile | FutureSourceFile, program: Program): Comparison { if (startsWith(a, "node:") && !startsWith(b, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.LessThan : Comparison.GreaterThan; if (startsWith(b, "node:") && !startsWith(a, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.GreaterThan : Comparison.LessThan; return Comparison.EqualTo; @@ -1064,7 +1069,7 @@ function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { * * @internal */ -export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { +export function getImportKind(importingFile: SourceFile | FutureSourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { if (compilerOptions.verbatimModuleSyntax && (getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS || importingFile.impliedNodeFormat === ModuleKind.CommonJS)) { // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible return ImportKind.CommonJS; @@ -1078,7 +1083,7 @@ export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, } } -function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getUmdImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { // Import a synthetic `default` if enabled. if (getAllowSyntheticDefaultImports(compilerOptions)) { return ImportKind.Default; @@ -1090,8 +1095,8 @@ function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOp case ModuleKind.AMD: case ModuleKind.CommonJS: case ModuleKind.UMD: - if (isInJSFile(importingFile)) { - return isExternalModule(importingFile) || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; + if (importingFile.kind ? isInJSFile(importingFile) : hasJSFileExtension(importingFile.fileName)) { + return importingFile.externalModuleIndicator || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; } return ImportKind.CommonJS; case ModuleKind.System: @@ -1206,9 +1211,9 @@ function getExportInfos( return originalSymbolToExportInfos; } -function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getExportEqualsImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { const allowSyntheticDefaults = getAllowSyntheticDefaultImports(compilerOptions); - const isJS = isInJSFile(importingFile); + const isJS = importingFile.kind ? isInJSFile(importingFile) : hasJSFileExtension(importingFile.fileName); // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. if (!isJS && getEmitModuleKind(compilerOptions) >= ModuleKind.ES2015) { @@ -1217,17 +1222,19 @@ function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: C // 2. 'import =' will not work in JavaScript, so the decision is between a default import, // a namespace import, and const/require. if (isJS) { - return isExternalModule(importingFile) || forceImportKeyword + return importingFile.externalModuleIndicator || forceImportKeyword ? allowSyntheticDefaults ? ImportKind.Default : ImportKind.Namespace : ImportKind.CommonJS; } // 3. At this point the most correct choice is probably 'import =', but people // really hate that, so look to see if the importing file has any precedent // on how to handle it. - for (const statement of importingFile.statements) { - // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration - if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { - return ImportKind.CommonJS; + if (importingFile.kind) { + for (const statement of importingFile.statements) { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { + return ImportKind.CommonJS; + } } } // 4. We have no precedent to go on, so just use a default import if @@ -1243,7 +1250,7 @@ function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: S return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports); } function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): DiagnosticOrDiagnosticAndArguments { - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); switch (fix.kind) { case ImportFixKind.UseNamespace: addNamespaceQualifier(changes, sourceFile, fix); diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 75f72964530b6..5972b29ab1a1a 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -2,7 +2,7 @@ import { AnyImportSyntax, Diagnostics, Expression, - getQuotePreference, + getQuotePreferenceFromFile, getTokenAtPosition, Identifier, isExternalModuleReference, @@ -57,5 +57,5 @@ function getInfo(sourceFile: SourceFile, pos: number): Info | undefined { } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info, preferences: UserPreferences): void { - changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreference(sourceFile, preferences))); + changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, preferences))); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 43ae8c2497a1c..01fa6ea982a59 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -96,7 +96,7 @@ import { getNewLineKind, getNewLineOrDefaultFromHost, getPropertyNameForPropertyNameNode, - getQuotePreference, + getQuotePreferenceFromFile, getReplacementSpanForContextToken, getRootDeclaration, getSourceFileOfModule, @@ -790,7 +790,7 @@ function continuePreviousIncompleteResponse( const touchNode = getTouchingPropertyName(file, position); const lowerCaseTokenText = location.text.toLowerCase(); - const exportMap = getExportInfoMap(file, host, program, preferences, cancellationToken); + const exportMap = getExportInfoMap(file.path, host, program, preferences, cancellationToken); const newEntries = resolvingModuleSpecifiers( "continuePreviousIncompleteResponse", host, @@ -1105,7 +1105,7 @@ function getJSDocParamAnnotation( const inferredType = checker.getTypeAtLocation(initializer.parent); if (!(inferredType.flags & (TypeFlags.Any | TypeFlags.Void))) { const sourceFile = initializer.getSourceFile(); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const builderFlags = (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); const typeNode = checker.typeToTypeNode(inferredType, findAncestor(initializer, isFunctionLike), builderFlags); if (typeNode) { @@ -1353,7 +1353,7 @@ function getExhaustiveCaseSnippets( const tracker = newCaseClauseTracker(checker, clauses); const target = getEmitScriptTarget(options); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); const elements: Expression[] = []; for (const type of switchType.types as LiteralType[]) { @@ -2048,7 +2048,7 @@ function createObjectLiteralMethod( const declaration = declarations[0]; const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const builderFlags = NodeBuilderFlags.OmitThisParameter | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); switch (declaration.kind) { @@ -3747,7 +3747,7 @@ function getCompletionData( ""; const moduleSpecifierCache = host.getModuleSpecifierCache?.(); - const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken); + const exportInfo = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken); const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host); resolvingModuleSpecifiers( diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 4651c1dac1c36..d1029858b0a45 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -451,7 +451,7 @@ function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly So } /** @internal */ -export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { +export function getExportInfoMap(importingFilePath: Path, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { const start = timestamp(); // Pulling the AutoImportProvider project will trigger its updateGraph if pending, // which will invalidate the export map cache if things change, so pull it before @@ -463,7 +463,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), }); - if (cache.isUsableByFile(importingFile.path)) { + if (cache.isUsableByFile(importingFilePath)) { host.log?.("getExportInfoMap: cache hit"); return cache; } @@ -481,7 +481,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic // can cause it to happen: see 'completionsImport_mergedReExport.ts' if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { cache.add( - importingFile.path, + importingFilePath, defaultInfo.symbol, defaultInfo.exportKind === ExportKind.Default ? InternalSymbolName.Default : InternalSymbolName.ExportEquals, moduleSymbol, @@ -493,7 +493,7 @@ export function getExportInfoMap(importingFile: SourceFile, host: LanguageServic checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, key)) { cache.add( - importingFile.path, + importingFilePath, exported, key, moduleSymbol, diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 8848d0fda2941..82ab390446eca 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -6,7 +6,7 @@ import { emptyArray, getBaseFileName, getLocaleSpecificMessage, - getQuotePreference, + getQuotePreferenceFromFile, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, @@ -82,7 +82,7 @@ function getNewStatementsAndRemoveFromOldFile( } const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreference(oldFile, preferences); + const quotePreference = getQuotePreferenceFromFile(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index d4f21e2f24ad7..52694145b554b 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -23,6 +23,7 @@ import { DeclarationStatement, EmitHint, EmitTextWriter, + emptyArray, endsWith, Expression, factory, @@ -40,6 +41,7 @@ import { formatting, FunctionDeclaration, FunctionExpression, + FutureSourceFile, getAncestor, getFirstNonSpaceCharacterPosition, getFormatCodeSettingsForWriting, @@ -337,6 +339,12 @@ interface ChangeText extends BaseChange { readonly text: string; } +interface NewFileInsertion { + readonly fileName: string; + readonly oldFile?: SourceFile; + readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]; +} + function getAdjustedRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd): TextRange { return { pos: getAdjustedStartPosition(sourceFile, startNode, options), end: getAdjustedEndPosition(sourceFile, endNode, options) }; } @@ -480,7 +488,7 @@ export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): /** @internal */ export class ChangeTracker { private readonly changes: Change[] = []; - private readonly newFiles: { readonly oldFile: SourceFile | undefined, readonly fileName: string, readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[] }[] = []; + private newFileChanges?: NewFileInsertion[]; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; @@ -600,28 +608,42 @@ export class ChangeTracker { this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options); } - public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void { + public insertNodeAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNode: Statement, blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween); } - public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { + public insertNodesAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween); } - private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { - const pos = getInsertionPositionAtSourceFileTop(sourceFile); + private insertAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { + const pos = sourceFile.kind ? getInsertionPositionAtSourceFileTop(sourceFile) : 0; const options = { prefix: pos === 0 ? undefined : this.newLineCharacter, - suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), + suffix: (sourceFile.kind && isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), }; if (isArray(insert)) { - this.insertNodesAt(sourceFile, pos, insert, options); + if (sourceFile.kind) { + this.insertNodesAt(sourceFile, pos, insert, options); + } + else { + this.insertStatementsInNewFile(sourceFile.fileName, insert); + } } else { - this.insertNodeAt(sourceFile, pos, insert, options); + if (sourceFile.kind) { + this.insertNodeAt(sourceFile, pos, insert, options); + } + else { + this.insertStatementsInNewFile(sourceFile.fileName, [insert]); + } } } + private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { + (this.newFileChanges ??= []).push({ fileName, statements, oldFile }); + } + public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray, newParam: ParameterDeclaration): void { const p0 = firstOrUndefined(parameters); if (p0) { @@ -1128,14 +1150,14 @@ export class ChangeTracker { this.finishDeleteDeclarations(); this.finishClassesWithNodesInsertedAtStart(); const changes = changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate); - for (const { oldFile, fileName, statements } of this.newFiles) { + for (const { oldFile, fileName, statements } of this.newFileChanges ?? emptyArray) { changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); } return changes; } - public addToNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { - this.newFiles.push({ oldFile, fileName, statements }); + public createNewFile(oldFile: SourceFile | undefined, fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]): void { + this.insertStatementsInNewFile(fileName, statements, oldFile); } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 8c483722f873f..0ce869031f6a3 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -85,11 +85,13 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, + FutureSourceFile, getAssignmentDeclarationKind, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, getEmitScriptTarget, getExternalModuleImportEqualsDeclarationExpression, + getImpliedNodeFormatForFile, getIndentString, getJSDocEnumTag, getLastChild, @@ -267,6 +269,8 @@ import { ModifierFlags, ModuleDeclaration, ModuleInstanceState, + ModuleKind, + ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierResolutionHost, moduleSpecifiers, @@ -350,6 +354,7 @@ import { textSpanEnd, Token, tokenToString, + toPath, tryCast, Type, TypeChecker, @@ -2472,9 +2477,14 @@ export function quotePreferenceFromString(str: StringLiteral, sourceFile: Source } /** @internal */ -export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { +export function getQuotePreference(preferences: UserPreferences): QuotePreference { + return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; +} + +/** @internal */ +export function getQuotePreferenceFromFile(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { if (preferences.quotePreference && preferences.quotePreference !== "auto") { - return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; + return getQuotePreference(preferences); } else { // ignore synthetic import added when importHelpers: true @@ -2561,17 +2571,18 @@ export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | und } /** @internal */ -export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { +export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile | FutureSourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; - const existingImportStatements = filter(sourceFile.statements, importKindPredicate); + const existingImportStatements = sourceFile.kind ? filter(sourceFile.statements, importKindPredicate) : undefined; let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; - if (!existingImportStatements.length) { + if (!existingImportStatements?.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { + Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); @@ -2590,6 +2601,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So else { const lastExistingImport = lastOrUndefined(existingImportStatements); if (lastExistingImport) { + Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); changes.insertNodesAfter(sourceFile, lastExistingImport, sortedNewImports); } else { @@ -3326,7 +3338,7 @@ export function getContextualTypeFromParent(node: Expression, checker: TypeCheck /** @internal */ export function quote(sourceFile: SourceFile, preferences: UserPreferences, text: string): string { // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - const quotePreference = getQuotePreference(sourceFile, preferences); + const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); const quoted = JSON.stringify(text); return quotePreference === QuotePreference.Single ? `'${stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted; } @@ -3687,7 +3699,7 @@ export interface PackageJsonImportFilter { } /** @internal */ -export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { +export function createPackageJsonImportFilter(fromFile: SourceFile | FutureSourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { const packageJsons = ( (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) ).filter(p => p.parseable); @@ -3787,7 +3799,7 @@ export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: // from Node core modules or not. We can start by seeing if the user is actually using // any node core modules, as opposed to simply having @types/node accidentally as a // dependency of a dependency. - if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { + if (fromFile.kind && isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { if (usesNodeCoreModules === undefined) { usesNodeCoreModules = consumesNodeCoreModules(fromFile); } @@ -4045,8 +4057,8 @@ export function isDeprecatedDeclaration(decl: Declaration) { } /** @internal */ -export function shouldUseUriStyleNodeCoreModules(file: SourceFile, program: Program): boolean { - const decisionFromFile = firstDefined(file.imports, node => { +export function shouldUseUriStyleNodeCoreModules(file: SourceFile | FutureSourceFile, program: Program): boolean { + const decisionFromFile = file.kind && firstDefined(file.imports, node => { if (JsTyping.nodeCoreModules.has(node.text)) { return startsWith(node.text, "node:"); } @@ -4068,6 +4080,27 @@ export function diagnosticToString(diag: DiagnosticOrDiagnosticAndArguments): st : getLocaleSpecificMessage(diag); } +/** @internal */ +export function createFutureSourceFile( + fileName: string, + program: Program, + host: ModuleResolutionHost, + moduleSyntax?: ModuleKind.CommonJS | ModuleKind.ESNext, +): FutureSourceFile { + const path = toPath(fileName, /*basePath*/ undefined, program.getCanonicalFileName); + const impliedNodeFormat = getImpliedNodeFormatForFile(path, program.getPackageJsonInfoCache?.(), host, program.getCompilerOptions()); + if (moduleSyntax === ModuleKind.CommonJS && impliedNodeFormat === ModuleKind.ESNext) { + Debug.fail("ES Modules cannot contain CommonJS syntax"); + } + return { + fileName, + path, + commonJsModuleIndicator: moduleSyntax === ModuleKind.CommonJS, + externalModuleIndicator: moduleSyntax === ModuleKind.ESNext, + impliedNodeFormat, + }; +} + /** * Get format code settings for a code writing context (e.g. when formatting text changes or completions code). * From 52674ac7fa6a5e45692a6ab7fa0d2b1130848f05 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Fri, 7 Apr 2023 14:44:00 -0700 Subject: [PATCH 25/42] importAdder changes for MoveToFile --- src/services/codefixes/importFixes.ts | 2 +- src/services/moveToFileAndNewFile.ts | 7 +++-- src/services/refactors/moveToFile.ts | 37 +++++++++++-------------- src/services/textChanges.ts | 39 ++++++++++++++++----------- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index feeeceed0b749..ddcc08c48ee64 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -200,7 +200,7 @@ export interface ImportAdder { } /** @internal */ -export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { +export function createImportAdder(sourceFile: SourceFile | FutureSourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host, cancellationToken); } diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts index 9a0b5c8d46465..001604e326c08 100644 --- a/src/services/moveToFileAndNewFile.ts +++ b/src/services/moveToFileAndNewFile.ts @@ -1,7 +1,7 @@ import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, flatMap, GetCanonicalFileName, getRangesWhere, last,length,mapDefined,some,tryCast } from "../compiler/core"; import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; -import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; +import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, ModuleKind, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, copyEntries, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachEntry,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSymbolId,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; /**@internal */ @@ -511,6 +511,7 @@ export interface UsageInfo { readonly oldImportsNeededByNewFile: ReadonlySymbolSet; // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. readonly unusedImportsFromOldFile: ReadonlySymbolSet; + readonly moduleSyntax: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; } type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' /**@internal */ @@ -623,9 +624,11 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], const movedSymbols = new SymbolSet(); const oldImportsNeededByNewFile = new SymbolSet(); const newFileImportsFromOldFile = new SymbolSet(); + const moduleSyntax = (oldFile.commonJsModuleIndicator) ? ModuleKind.CommonJS : (oldFile.externalModuleIndicator) ? ModuleKind.ESNext : undefined; 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); } @@ -666,7 +669,7 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], }); } - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile, moduleSyntax }; function getJsxNamespaceSymbol(containsJsx: Node | undefined) { if (containsJsx === undefined) { diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 320282dc8b509..7237702f5f643 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -3,11 +3,12 @@ import { append, ApplicableRefactorInfo, codefix, + createFutureSourceFile, Debug, Diagnostics, emptyArray, getLocaleSpecificMessage, - getQuotePreference, + getQuotePreferenceFromFile, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, @@ -28,7 +29,7 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; @@ -69,20 +70,21 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string const usage = getUsageInfo(oldFile, toMove.all, checker); //creating a new file if (!host.fileExists(newFile)) { - changes.addToNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); + const importAdder = codefix.createImportAdder(createFutureSourceFile(newFile, program, host, usage.moduleSyntax), context.program, context.preferences, context.host); + changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false, importAdder)); addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); } else { const sourceFile = program.getSourceFile(newFile); if (sourceFile) { - const newFileImportAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); - getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, newFileImportAdder); + const importAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); + getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, importAdder); } } } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder + oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder: codefix.ImportAdder ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); @@ -92,7 +94,7 @@ function getNewStatementsAndRemoveFromOldFile( } const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreference(oldFile, preferences); + const quotePreference = getQuotePreferenceFromFile(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); @@ -143,21 +145,12 @@ function getNewFileImportsAndAddExportInOldFile( host: LanguageServiceHost, useEsModuleSyntax: boolean, quotePreference: QuotePreference, - newFileImportAdder?: codefix.ImportAdder, + importAdder: codefix.ImportAdder, ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; - if (newFileImportAdder) { - importsToCopy.forEach(symbol => { - newFileImportAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); - }); - } - else { - for (const oldStatement of oldFile.statements) { - forEachImportInStatement(oldStatement, i => { - append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); - }); - } - } + importsToCopy.forEach(symbol => { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); + }); //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. const newFileSourceFile = program.getSourceFile(newFile); @@ -177,8 +170,8 @@ function getNewFileImportsAndAddExportInOldFile( if (markSeenTop(top)) { addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); } - if (newFileImportAdder && symbol.parent !== undefined) { - newFileImportAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); + if (importAdder && symbol.parent !== undefined) { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); } else { if (hasSyntacticModifier(decl, ModifierFlags.Default)) { diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 52694145b554b..168a36b591d77 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -10,6 +10,7 @@ import { concatenate, ConstructorDeclaration, contains, + createMultiMap, createNodeFactory, createPrinter, createRange, @@ -23,7 +24,6 @@ import { DeclarationStatement, EmitHint, EmitTextWriter, - emptyArray, endsWith, Expression, factory, @@ -118,6 +118,7 @@ import { mapDefined, MethodSignature, Modifier, + MultiMap, NamedImportBindings, NamedImports, NamespaceImport, @@ -340,7 +341,6 @@ interface ChangeText extends BaseChange { } interface NewFileInsertion { - readonly fileName: string; readonly oldFile?: SourceFile; readonly statements: readonly (Statement | SyntaxKind.NewLineTrivia)[]; } @@ -488,7 +488,7 @@ export function isThisTypeAnnotatable(containingFunction: SignatureDeclaration): /** @internal */ export class ChangeTracker { private readonly changes: Change[] = []; - private newFileChanges?: NewFileInsertion[]; + private newFileChanges?: MultiMap ; private readonly classesWithNodesInsertedAtStart = new Map(); // Set implemented as Map private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray }[] = []; @@ -641,7 +641,10 @@ export class ChangeTracker { } private insertStatementsInNewFile(fileName: string, statements: readonly (Statement | SyntaxKind.NewLineTrivia)[], oldFile?: SourceFile): void { - (this.newFileChanges ??= []).push({ fileName, statements, oldFile }); + if (!this.newFileChanges) { + this.newFileChanges = createMultiMap(); + } + this.newFileChanges.add(fileName, { oldFile, statements }); } public insertFirstParameter(sourceFile: SourceFile, parameters: NodeArray, newParam: ParameterDeclaration): void { @@ -1150,8 +1153,19 @@ 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.newFileChanges ?? emptyArray) { - changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); + // for (const { oldFile, fileName, statements } of this.newFileChanges ?? emptyArray) { + // changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); + // } + if (this.newFileChanges) { + this.newFileChanges.forEach((insertions, fileName) => { + // const textChanges: TextChange[] = []; + // for (const { oldFile, statements } of newFileInsertion) { + // const fileTextChanges = changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext); + // textChanges.push(...fileTextChanges.textChanges); + // } + + changes.push(changesToText.newFileChanges(fileName, insertions, this.newLineCharacter, this.formatContext)); + }); } return changes; } @@ -1229,11 +1243,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 => { @@ -1262,14 +1271,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; From 566621802ed11fa234e0c3f17431d6c7354d5398 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Sun, 9 Apr 2023 23:08:11 -0700 Subject: [PATCH 26/42] adding, cleaning up tests --- src/services/refactors/moveToFile.ts | 29 +++++---- src/services/refactors/moveToNewFile.ts | 2 +- src/services/textChanges.ts | 9 --- tests/cases/fourslash/moveToFile5.ts | 45 -------------- tests/cases/fourslash/moveToFile6.ts | 49 --------------- tests/cases/fourslash/moveToFile7_newFile.ts | 40 ------------ .../moveToFile8_multipleStatements.ts | 61 ------------------- .../moveToFile9_differentDirectories.ts | 52 ---------------- .../moveToFile_addImportFromTargetFile.ts | 33 ++++++++++ .../fourslash/moveToFile_blankExistingFile.ts | 28 +++++++++ ...e10_ctsTomts.ts => moveToFile_ctsTomts.ts} | 23 ++----- .../moveToFile_differentDirectories.ts | 32 ++++++++++ .../moveToFile_multipleStatements.ts | 36 +++++++++++ ...eToFile1.ts => moveToFile_namedImports.ts} | 2 + tests/cases/fourslash/moveToFile_newFile.ts | 20 ++++++ ...e4.ts => moveToFile_nonExportedImports.ts} | 16 +---- .../fourslash/moveToFile_requireImport.ts | 44 +++++++++++++ ...veToFile2.ts => moveToFile_typeImport1.ts} | 18 +++--- ...veToFile3.ts => moveToFile_typeImport2.ts} | 15 +---- 19 files changed, 232 insertions(+), 322 deletions(-) delete mode 100644 tests/cases/fourslash/moveToFile5.ts delete mode 100644 tests/cases/fourslash/moveToFile6.ts delete mode 100644 tests/cases/fourslash/moveToFile7_newFile.ts delete mode 100644 tests/cases/fourslash/moveToFile8_multipleStatements.ts delete mode 100644 tests/cases/fourslash/moveToFile9_differentDirectories.ts create mode 100644 tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts create mode 100644 tests/cases/fourslash/moveToFile_blankExistingFile.ts rename tests/cases/fourslash/{moveToFile10_ctsTomts.ts => moveToFile_ctsTomts.ts} (58%) create mode 100644 tests/cases/fourslash/moveToFile_differentDirectories.ts create mode 100644 tests/cases/fourslash/moveToFile_multipleStatements.ts rename tests/cases/fourslash/{moveToFile1.ts => moveToFile_namedImports.ts} (93%) create mode 100644 tests/cases/fourslash/moveToFile_newFile.ts rename tests/cases/fourslash/{moveToFile4.ts => moveToFile_nonExportedImports.ts} (60%) create mode 100644 tests/cases/fourslash/moveToFile_requireImport.ts rename tests/cases/fourslash/{moveToFile2.ts => moveToFile_typeImport1.ts} (50%) rename tests/cases/fourslash/{moveToFile3.ts => moveToFile_typeImport2.ts} (60%) diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 7237702f5f643..3330d1d0adb86 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -3,7 +3,6 @@ import { append, ApplicableRefactorInfo, codefix, - createFutureSourceFile, Debug, Diagnostics, emptyArray, @@ -29,7 +28,7 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; @@ -69,9 +68,8 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); //creating a new file - if (!host.fileExists(newFile)) { - const importAdder = codefix.createImportAdder(createFutureSourceFile(newFile, program, host, usage.moduleSyntax), context.program, context.preferences, context.host); - changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false, importAdder)); + if (!host.fileExists(newFile) || host.fileExists(newFile) && program.getSourceFile(newFile)?.statements.length === 0) { + changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); } else { @@ -84,11 +82,11 @@ function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder: codefix.ImportAdder + oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder ) { 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.oldImportsNeededByNewFile.size() === 0 && !newFileExists) { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } @@ -145,12 +143,21 @@ function getNewFileImportsAndAddExportInOldFile( host: LanguageServiceHost, useEsModuleSyntax: boolean, quotePreference: QuotePreference, - importAdder: codefix.ImportAdder, + importAdder?: codefix.ImportAdder, ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; - importsToCopy.forEach(symbol => { - importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); - }); + if (importAdder) { + importsToCopy.forEach(symbol => { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); + }); + } + else { + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); + } + } //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. const newFileSourceFile = program.getSourceFile(newFile); diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 82ab390446eca..9a9bbbf30143f 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -66,7 +66,7 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes const newFilename = createNewFilename(oldFile, program, context, host); // If previous file was global, this is easy. - changes.addToNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences)); + changes.createNewFile(oldFile, newFilename, getNewStatementsAndRemoveFromOldFile(oldFile, usage, changes, toMove, program, host, newFilename, preferences)); addNewFileToTsconfig(program, changes, oldFile.fileName, newFilename, hostGetCanonicalFileName(host)); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 168a36b591d77..493955b2a2f08 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -1153,17 +1153,8 @@ 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.newFileChanges ?? emptyArray) { - // changes.push(changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext)); - // } if (this.newFileChanges) { this.newFileChanges.forEach((insertions, fileName) => { - // const textChanges: TextChange[] = []; - // for (const { oldFile, statements } of newFileInsertion) { - // const fileTextChanges = changesToText.newFileChanges(oldFile, fileName, statements, this.newLineCharacter, this.formatContext); - // textChanges.push(...fileTextChanges.textChanges); - // } - changes.push(changesToText.newFileChanges(fileName, insertions, this.newLineCharacter, this.formatContext)); }); } diff --git a/tests/cases/fourslash/moveToFile5.ts b/tests/cases/fourslash/moveToFile5.ts deleted file mode 100644 index d9ddda759760f..0000000000000 --- a/tests/cases/fourslash/moveToFile5.ts +++ /dev/null @@ -1,45 +0,0 @@ -/// - -//@Filename: /bar.ts -////import './someFile'; -//// -////const q = 20; - -// @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const y: Date = p + b;|] - -// @Filename: /other.ts -////export const b = 2; -////export const a = 1; -////export const alreadyUnused = "unused"; - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`// header comment - -import './foo'; -import { a, alreadyUnused } from './other'; -export const p = 0; -`, - - "/bar.ts": -`import { p } from './a'; -import { b } from './other'; -import './someFile'; - -const q = 20; -const y: Date = p + b; -`, - }, - newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile6.ts b/tests/cases/fourslash/moveToFile6.ts deleted file mode 100644 index be5af498ba864..0000000000000 --- a/tests/cases/fourslash/moveToFile6.ts +++ /dev/null @@ -1,49 +0,0 @@ -/// - -//@Filename: /bar.ts -////import './blah'; -////import './blah2'; -////const a = 2; -////a; - -// @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const y: Date = p + b;|] -////a; y; - -// @Filename: /other.ts -////export const b = 1; -////export const a = 2; -////export const alreadyUnused = "unused"; - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`// header comment - -import { y } from './bar'; -import './foo'; -import { a, alreadyUnused } from './other'; -export const p = 0; -a; y;`, - - "/bar.ts": -`import { p } from './a'; -import './blah'; -import './blah2'; -import { b } from './other'; -const a = 2; -a; -export const y: Date = p + b; -`, - }, - newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile7_newFile.ts b/tests/cases/fourslash/moveToFile7_newFile.ts deleted file mode 100644 index 56afa1a039d11..0000000000000 --- a/tests/cases/fourslash/moveToFile7_newFile.ts +++ /dev/null @@ -1,40 +0,0 @@ -/// - -// @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const y: Date = p + b;|] -////a; y; - -// @Filename: /other.ts -////export const b = 1; -////export const a = 2; -////export const alreadyUnused = "unused"; - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`// header comment - -import { y } from './bar'; -import './foo'; -import { a, alreadyUnused } from './other'; -export const p = 0; -a; y;`, - - "/bar.ts": -`import { b } from './other'; -import { p } from './a'; - -export const y: Date = p + b; -`, - }, - newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile8_multipleStatements.ts b/tests/cases/fourslash/moveToFile8_multipleStatements.ts deleted file mode 100644 index ce9691577c234..0000000000000 --- a/tests/cases/fourslash/moveToFile8_multipleStatements.ts +++ /dev/null @@ -1,61 +0,0 @@ -/// - -//@Filename: /bar.ts -////import './blah'; -////import './blah2'; -////const a = 2; -////a; - -// @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const q = 0; -////const t = 2; -////function f() {} -////class C {} -////enum E {} -////namespace N { export const h = 0; } -////type T = number; -////interface I {}|] -////t; - -// @Filename: /other.ts -////export const b = 1; -////export const a = 2; -////export const alreadyUnused = "unused"; - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`// header comment - -import { t } from './bar'; -import './foo'; -import { a, b, alreadyUnused } from './other'; -const p = 0; -t;`, - - "/bar.ts": -`import './blah'; -import './blah2'; -const a = 2; -a; -const q = 0; -export const t = 2; -function f() { } -class C { } -enum E { } -namespace N { export const h = 0; } -type T = number; -interface I { } -`, - }, - newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile9_differentDirectories.ts b/tests/cases/fourslash/moveToFile9_differentDirectories.ts deleted file mode 100644 index 06181e2a51853..0000000000000 --- a/tests/cases/fourslash/moveToFile9_differentDirectories.ts +++ /dev/null @@ -1,52 +0,0 @@ -/// - -//@moduleResolution: bundler -//@module: esnext - -//@Filename: /src/dir2/bar.ts -////import './blah'; -////import './blah2'; -////const a = 2; -////a; - -// @Filename: /src/dir1/a.ts -////// header comment -//// -////import './foo'; -////import { a, b, alreadyUnused } from './other'; -////const p = 0; -////[|const y: Date = p + b;|] -////a; y; - -// @Filename: /src/dir1/other.ts -////export const b = 1; -////export const a = 2; -////export const alreadyUnused = "unused"; - -verify.moveToFile({ - newFileContents: { - "/src/dir1/a.ts": -`// header comment - -import { y } from '../dir2/bar'; -import './foo'; -import { a, alreadyUnused } from './other'; -export const p = 0; -a; y;`, - - "/src/dir2/bar.ts": -`import { p } from '../dir1/a'; -import { b } from '../dir1/other'; -import './blah'; -import './blah2'; -const a = 2; -a; -export const y: Date = p + b; -`, - }, - newFile: "/src/dir2/bar.ts", - - preferences: { - quotePreference: "single", - } -}); diff --git a/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts new file mode 100644 index 0000000000000..5c9ca10795fe4 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts @@ -0,0 +1,33 @@ +/// + +//@Filename: /bar.ts +////import './blah'; +//// +////const a = 2; + +// @Filename: /a.ts +////import { b } from './other'; +//// +////[|const y = b + 10;|] +////y; + +// @Filename: /other.ts +////export const b = 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`import { y } from './bar'; + +y;`, + + "/bar.ts": +`import './blah'; +import { b } from './other'; + +const a = 2; +export const y = b + 10; +`, + }, + newFile: "/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile_blankExistingFile.ts b/tests/cases/fourslash/moveToFile_blankExistingFile.ts new file mode 100644 index 0000000000000..9a96731711356 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_blankExistingFile.ts @@ -0,0 +1,28 @@ +/// + +// @Filename: /bar.ts +////// + +// @Filename: /a.ts +////import { b } from './other'; +////const p = 0; +////[|const y: Date = p + b;|] + +// @Filename: /other.ts +////export const b = 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`export const p = 0; +`, + + "/bar.ts": +`import { b } from './other'; +import { p } from './a'; + +const y: Date = p + b; +`, + }, + newFile: "/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile10_ctsTomts.ts b/tests/cases/fourslash/moveToFile_ctsTomts.ts similarity index 58% rename from tests/cases/fourslash/moveToFile10_ctsTomts.ts rename to tests/cases/fourslash/moveToFile_ctsTomts.ts index 464fa2d316e8d..250ddcbe3c9b4 100644 --- a/tests/cases/fourslash/moveToFile10_ctsTomts.ts +++ b/tests/cases/fourslash/moveToFile_ctsTomts.ts @@ -6,46 +6,31 @@ //@Filename: /src/dir2/bar.mts ////import './blah.ts'; -////import './blah2.ts'; ////const a = 2; -////a; // @Filename: /src/dir1/a.cts -////// header comment -//// -////import { a, b, alreadyUnused } from './other.cts'; +////import { b } from './other.cts'; ////const p = 0; ////[|const y = p + b;|] -////a; y; +////y; // @Filename: /src/dir1/other.cts ////export const b = 1; -////export const a = 2; -////export const alreadyUnused = "unused"; verify.moveToFile({ newFileContents: { "/src/dir1/a.cts": -`// header comment - -import { y } from '../dir2/bar.mts'; -import { a, alreadyUnused } from './other.cts'; +`import { y } from '../dir2/bar.mts'; export const p = 0; -a; y;`, +y;`, "/src/dir2/bar.mts": `import { p } from '../dir1/a.cts'; import { b } from '../dir1/other.cts'; import './blah.ts'; -import './blah2.ts'; const a = 2; -a; export const y = p + b; `, }, newFile: "/src/dir2/bar.mts", - - preferences: { - quotePreference: "single", - } }); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories.ts b/tests/cases/fourslash/moveToFile_differentDirectories.ts new file mode 100644 index 0000000000000..4c1faa4ee7b25 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_differentDirectories.ts @@ -0,0 +1,32 @@ +/// + +//@moduleResolution: bundler +//@module: esnext + +// @Filename: /src/dir1/a.ts +////import { b } from './other'; +////[|const y = b + 10;|] +////y; + +// @Filename: /src/dir1/other.ts +////export const b = 1; + +//@Filename: /src/dir2/bar.ts +////import './blah'; +////const a = 2; + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.ts": +`import { y } from '../dir2/bar'; +y;`, + + "/src/dir2/bar.ts": +`import { b } from '../dir1/other'; +import './blah'; +const a = 2; +export const y = b + 10; +`, + }, + newFile: "/src/dir2/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile_multipleStatements.ts b/tests/cases/fourslash/moveToFile_multipleStatements.ts new file mode 100644 index 0000000000000..2286be7b16c53 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_multipleStatements.ts @@ -0,0 +1,36 @@ +/// + +//@Filename: /bar.ts +////import { } from './somefile'; +////const a = 12; + +// @Filename: /a.ts +////[|const q = 0; +////const t = 2; +////function f() {} +////class C {} +////enum E {} +////namespace N { export const h = 0; } +////type T = number; +////interface I {}|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`import { } from './somefile'; +const a = 12; +const q = 0; +const t = 2; +function f() { } +class C { } +enum E { } +namespace N { export const h = 0; } +type T = number; +interface I { } +`, + }, + newFile: "/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile1.ts b/tests/cases/fourslash/moveToFile_namedImports.ts similarity index 93% rename from tests/cases/fourslash/moveToFile1.ts rename to tests/cases/fourslash/moveToFile_namedImports.ts index 7043ef3aad3b4..8df7383dda501 100644 --- a/tests/cases/fourslash/moveToFile1.ts +++ b/tests/cases/fourslash/moveToFile_namedImports.ts @@ -9,6 +9,7 @@ //// ////import { } from './other'; ////import type { } from './other'; +//// ////export const p = 0; ////export const b = 1; ////[|const y = p + b;|] @@ -24,6 +25,7 @@ verify.moveToFile({ import { } from './other'; import type { } from './other'; + export const p = 0; export const b = 1; `, diff --git a/tests/cases/fourslash/moveToFile_newFile.ts b/tests/cases/fourslash/moveToFile_newFile.ts new file mode 100644 index 0000000000000..54bbcae350dec --- /dev/null +++ b/tests/cases/fourslash/moveToFile_newFile.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////[|interface ka { +//// name: string; +////}|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`interface ka { + name: string; +} +`, + }, + newFile: "/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile4.ts b/tests/cases/fourslash/moveToFile_nonExportedImports.ts similarity index 60% rename from tests/cases/fourslash/moveToFile4.ts rename to tests/cases/fourslash/moveToFile_nonExportedImports.ts index d9b2b884ce721..63a30480dd917 100644 --- a/tests/cases/fourslash/moveToFile4.ts +++ b/tests/cases/fourslash/moveToFile_nonExportedImports.ts @@ -6,25 +6,17 @@ ////const q = 20; // @Filename: /a.ts -////// header comment -//// -////import './foo'; -////import { a, b } from './other'; +////import { b } from './other'; ////const p = 0; ////[|const y: Date = p + b;|] // @Filename: /other.ts ////export const b = 2; -////export const a = 1; verify.moveToFile({ newFileContents: { "/a.ts": -`// header comment - -import './foo'; -import { a } from './other'; -export const p = 0; +`export const p = 0; `, "/bar.ts": @@ -37,8 +29,4 @@ const y: Date = p + b; `, }, newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } }); diff --git a/tests/cases/fourslash/moveToFile_requireImport.ts b/tests/cases/fourslash/moveToFile_requireImport.ts new file mode 100644 index 0000000000000..d3adb9e19173a --- /dev/null +++ b/tests/cases/fourslash/moveToFile_requireImport.ts @@ -0,0 +1,44 @@ +/// + +// @module: commonjs +// @allowJs: true + +//@Filename: /bar.js +////const x = 0; + +//@Filename: /a.js +////const { a, b } = require("./other"); +////const p = 0; +////[|const y = p; +////const z = 0; +////exports.z = 0;|] +////a; y; z; + +//@Filename: /other.js +////const a = 1; +////exports.a = a; + +//@Filename: /user.ts +////const { x, y } = require("./a"); + +verify.moveToFile({ + newFileContents: { + "/a.js": +`const { y, z } = require("./bar"); +const { a, b } = require("./other"); +const p = 0; +exports.p = p; +a; y; z;`, + + "/bar.js": +`const { p } = require("./a"); + +const x = 0; +const y = p; +exports.y = y; +const z = 0; +exports.z = 0; +`, + }, + newFile: "/bar.js" +}); diff --git a/tests/cases/fourslash/moveToFile2.ts b/tests/cases/fourslash/moveToFile_typeImport1.ts similarity index 50% rename from tests/cases/fourslash/moveToFile2.ts rename to tests/cases/fourslash/moveToFile_typeImport1.ts index e37c122293868..5abb0023a5e7e 100644 --- a/tests/cases/fourslash/moveToFile2.ts +++ b/tests/cases/fourslash/moveToFile_typeImport1.ts @@ -2,13 +2,15 @@ /// // @Filename: /bar.ts -//// const q = 0; +////import {} from "./somefile"; // @Filename: /a.ts ////import { type A } from "./other"; -////[|function f(a: A) {}|] +////import type { B } from "./other"; +////[|function f(a: B) {}|] // @Filename: /other.ts +////export type B = {}; ////export interface A { //// x: number; ////} @@ -16,14 +18,14 @@ verify.moveToFile({ newFileContents: { - "/a.ts":"", + "/a.ts": +`import { type A } from "./other"; +`, "/bar.ts": -`import { A } from "./other"; - -const q = 0; -function f(a: A) { } +`import { B } from "./other"; +import {} from "./somefile"; +function f(a: B) { } `, }, newFile: "/bar.ts", - preferences: {} }); diff --git a/tests/cases/fourslash/moveToFile3.ts b/tests/cases/fourslash/moveToFile_typeImport2.ts similarity index 60% rename from tests/cases/fourslash/moveToFile3.ts rename to tests/cases/fourslash/moveToFile_typeImport2.ts index 75fc870ed66d2..cf38d2e24a855 100644 --- a/tests/cases/fourslash/moveToFile3.ts +++ b/tests/cases/fourslash/moveToFile_typeImport2.ts @@ -4,11 +4,8 @@ // @Filename: /bar.ts //// import type {} from './a'; //// -////const q = 0; // @Filename: /a.ts -////// header comment -//// ////export const p = 0; ////export const b = 1; ////[|const y = p + b;|] @@ -18,22 +15,14 @@ verify.moveToFile({ newFileContents: { "/a.ts": -`// header comment - -export const p = 0; +`export const p = 0; export const b = 1; `, "/bar.ts": -` import { b, p } from './a'; - -const q = 0; +`import { b, p } from './a'; const y = p + b; `, }, newFile: "/bar.ts", - - preferences: { - quotePreference: "single", - } }); From 9c5efd5ddfaf3f0ff514663a69c1235a72d97409 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 10 Apr 2023 00:35:27 -0700 Subject: [PATCH 27/42] Reverting changes for impoart adder for new file --- src/compiler/moduleSpecifiers.ts | 23 +++-- src/compiler/types.ts | 10 --- src/compiler/utilities.ts | 5 +- .../codefixes/convertFunctionToEs6Class.ts | 4 +- src/services/codefixes/convertToEsModule.ts | 6 +- src/services/codefixes/fixAddMissingMember.ts | 8 +- .../codefixes/fixInvalidImportSyntax.ts | 4 +- .../fixNoPropertyAccessFromIndexSignature.ts | 4 +- src/services/codefixes/helpers.ts | 6 +- src/services/codefixes/importFixes.ts | 83 +++++++++---------- src/services/codefixes/useDefaultImport.ts | 4 +- src/services/completions.ts | 12 +-- src/services/exportInfoMap.ts | 8 +- src/services/refactors/moveToFile.ts | 4 +- src/services/refactors/moveToNewFile.ts | 4 +- src/services/textChanges.ts | 25 ++---- src/services/utilities.ts | 53 +++--------- tests/cases/fourslash/moveToFile_ctsTomts.ts | 2 +- .../cases/fourslash/moveToFile_typeImport2.ts | 1 - 19 files changed, 101 insertions(+), 165 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index d982668b58177..87efee364c0d9 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -31,7 +31,6 @@ import { flatten, forEach, forEachAncestorDirectory, - FutureSourceFile, getBaseFileName, GetCanonicalFileName, getConditions, @@ -127,7 +126,7 @@ interface Preferences { function getPreferences( { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile | FutureSourceFile, + importingSourceFile: SourceFile, oldImportSpecifier?: string, ): Preferences { const preferredEnding = getPreferredEnding(); @@ -215,7 +214,7 @@ export function getModuleSpecifier( /** @internal */ export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFile: SourceFile | FutureSourceFile, + importingSourceFile: SourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, @@ -246,14 +245,14 @@ function getModuleSpecifierWorker( /** @internal */ export function tryGetModuleSpecifiersFromCache( moduleSymbol: Symbol, - importingSourceFilePath: Path, + importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] | undefined { return tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFilePath, + importingSourceFile, host, userPreferences, options)[0]; @@ -261,7 +260,7 @@ export function tryGetModuleSpecifiersFromCache( function tryGetModuleSpecifiersFromCacheWorker( moduleSymbol: Symbol, - importingSourceFilePath: Path, + importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -272,7 +271,7 @@ function tryGetModuleSpecifiersFromCacheWorker( } const cache = host.getModuleSpecifierCache?.(); - const cached = cache?.get(importingSourceFilePath, moduleSourceFile.path, userPreferences, options); + const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences, options); return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache]; } @@ -306,7 +305,7 @@ export function getModuleSpecifiersWithCacheInfo( moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - importingSourceFile: SourceFile | FutureSourceFile, + importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, @@ -318,7 +317,7 @@ export function getModuleSpecifiersWithCacheInfo( // eslint-disable-next-line prefer-const let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker( moduleSymbol, - importingSourceFile.path, + importingSourceFile, host, userPreferences, options @@ -336,14 +335,14 @@ export function getModuleSpecifiersWithCacheInfo( function computeModuleSpecifiers( modulePaths: readonly ModulePath[], compilerOptions: CompilerOptions, - importingSourceFile: SourceFile | FutureSourceFile, + importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, userPreferences: UserPreferences, options: ModuleSpecifierOptions = {}, ): readonly string[] { const info = getInfo(importingSourceFile.path, host); const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); - const existingSpecifier = importingSourceFile.kind && forEach(modulePaths, modulePath => forEach( + const existingSpecifier = forEach(modulePaths, modulePath => forEach( host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)), reason => { if (reason.kind !== FileIncludeKind.Import || reason.file !== importingSourceFile.path) return undefined; @@ -880,7 +879,7 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam return processEnding(shortest, allowedEndings, compilerOptions); } -function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile | FutureSourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { +function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0729ac9ff09ac..dc8d135b060a3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4223,16 +4223,6 @@ export interface SourceFileLike { getPositionOfLineAndCharacter?(line: number, character: number, allowEdits?: true): number; } -/** @internal */ -export interface FutureSourceFile { - readonly kind?: undefined; - readonly fileName: string; - readonly path: Path; - readonly impliedNodeFormat?: ResolutionMode; - readonly commonJsModuleIndicator?: boolean; - readonly externalModuleIndicator?: boolean; -} - /** @internal */ export interface RedirectInfo { /** Source file this redirects to. */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 45b2992f8d035..74d9e485e3afb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -157,7 +157,6 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, - FutureSourceFile, GetAccessorDeclaration, getBaseFileName, GetCanonicalFileName, @@ -9165,7 +9164,7 @@ export function usesExtensionsOnImports({ imports }: SourceFile, hasExtension: ( } /** @internal */ -export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile | FutureSourceFile): ModuleSpecifierEnding { +export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile): ModuleSpecifierEnding { if (preference === "js" || resolutionMode === ModuleKind.ESNext) { // Extensions are explicitly requested or required. Now choose between .js and .ts. if (!shouldAllowImportingTsExtension(compilerOptions)) { @@ -9190,7 +9189,7 @@ export function getModuleSpecifierEndingPreference(preference: UserPreferences[" // accurately, and more importantly, literally nobody wants `Index` and its existence is a mystery. if (!shouldAllowImportingTsExtension(compilerOptions)) { // If .ts imports are not valid, we only need to see one .js import to go with that. - return sourceFile.kind && usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; + return usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal; } return inferPreference(); diff --git a/src/services/codefixes/convertFunctionToEs6Class.ts b/src/services/codefixes/convertFunctionToEs6Class.ts index 5c46bed9c57d8..5073d687a67ed 100644 --- a/src/services/codefixes/convertFunctionToEs6Class.ts +++ b/src/services/codefixes/convertFunctionToEs6Class.ts @@ -21,7 +21,7 @@ import { FunctionExpression, getEmitScriptTarget, getNameOfDeclaration, - getQuotePreferenceFromFile, + getQuotePreference, getTokenAtPosition, idText, isAccessExpression, @@ -211,7 +211,7 @@ function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, po // f.x = expr if (isAccessExpression(memberDeclaration) && (isFunctionExpression(assignmentExpr) || isArrowFunction(assignmentExpr))) { - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const name = tryGetPropertyName(memberDeclaration, compilerOptions, quotePreference); if (name) { createFunctionLikeExpressionMember(members, assignmentExpr, name); diff --git a/src/services/codefixes/convertToEsModule.ts b/src/services/codefixes/convertToEsModule.ts index 1f8b08d5e9b45..b15a0521c1cb1 100644 --- a/src/services/codefixes/convertToEsModule.ts +++ b/src/services/codefixes/convertToEsModule.ts @@ -27,7 +27,7 @@ import { FunctionExpression, getEmitScriptTarget, getModeForUsageLocation, - getQuotePreferenceFromFile, + getQuotePreference, getResolvedModule, getSynthesizedDeepClone, getSynthesizedDeepClones, @@ -87,10 +87,10 @@ registerCodeFix({ getCodeActions(context) { const { sourceFile, program, preferences } = context; const changes = textChanges.ChangeTracker.with(context, changes => { - const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreferenceFromFile(sourceFile, preferences)); + const moduleExportsChangedToDefault = convertFileToEsModule(sourceFile, program.getTypeChecker(), changes, getEmitScriptTarget(program.getCompilerOptions()), getQuotePreference(sourceFile, preferences)); if (moduleExportsChangedToDefault) { for (const importingFile of program.getSourceFiles()) { - fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreferenceFromFile(importingFile, preferences)); + fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences)); } } }); diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 464d58653a748..3d46bc1511d2a 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -33,7 +33,7 @@ import { getNodeId, getObjectFlags, getOrUpdate, - getQuotePreferenceFromFile, + getQuotePreference, getSourceFileOfNode, getTokenAtPosition, hasAbstractModifier, @@ -597,7 +597,7 @@ function addEnumMemberDeclaration(changes: textChanges.ChangeTracker, checker: T } function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: FunctionInfo | SignatureInfo) { - const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); const functionDeclaration = info.kind === InfoKind.Function ? createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) @@ -614,7 +614,7 @@ function addFunctionDeclaration(changes: textChanges.ChangeTracker, context: Cod function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: JsxAttributesInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); const checker = context.program.getTypeChecker(); const jsxAttributesNode = info.parentDeclaration.attributes; const hasSpreadAttribute = some(jsxAttributesNode.properties, isJsxSpreadAttribute); @@ -634,7 +634,7 @@ function addJsxAttributes(changes: textChanges.ChangeTracker, context: CodeFixCo function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) { const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host); - const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); const target = getEmitScriptTarget(context.program.getCompilerOptions()); const checker = context.program.getTypeChecker(); const props = map(info.properties, prop => { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index 9a88b3b49fc64..bae3f2d2c0f16 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -8,7 +8,7 @@ import { findAncestor, getEmitModuleKind, getNamespaceDeclarationNode, - getQuotePreferenceFromFile, + getQuotePreference, getSourceFileOfNode, getTokenAtPosition, ImportDeclaration, @@ -39,7 +39,7 @@ function getCodeFixesForImportDeclaration(context: CodeFixContext, node: ImportD const variations: CodeFixAction[] = []; // import Bluebird from "bluebird"; - variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, context.preferences)))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreference(sourceFile, context.preferences)))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts index 1e517583bf85b..7a35126032924 100644 --- a/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts +++ b/src/services/codefixes/fixNoPropertyAccessFromIndexSignature.ts @@ -2,7 +2,7 @@ import { cast, Diagnostics, factory, - getQuotePreferenceFromFile, + getQuotePreference, getTokenAtPosition, isPropertyAccessChain, isPropertyAccessExpression, @@ -37,7 +37,7 @@ registerCodeFix({ }); function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: PropertyAccessExpression, preferences: UserPreferences): void { - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const argumentsExpression = factory.createStringLiteral(node.name.text, quotePreference === QuotePreference.Single); changes.replaceNode( sourceFile, diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 00a878b049de6..e47e4c99777a2 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -27,7 +27,7 @@ import { getModuleSpecifierResolverHost, getNameForExportedSymbol, getNameOfDeclaration, - getQuotePreferenceFromFile, + getQuotePreference, getSetAccessorValueParameter, getSynthesizedDeepClone, getTokenAtPosition, @@ -205,7 +205,7 @@ export function addNewNodeForMemberSymbol( const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); const optional = !!(symbol.flags & SymbolFlags.Optional); const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient; - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); switch (kind) { case SyntaxKind.PropertySignature: @@ -467,7 +467,7 @@ export function createSignatureDeclarationFromCallExpression( modifierFlags: ModifierFlags, contextNode: Node ): MethodDeclaration | FunctionDeclaration | MethodSignature { - const quotePreference = getQuotePreferenceFromFile(context.sourceFile, context.preferences); + const quotePreference = getQuotePreference(context.sourceFile, context.preferences); const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions()); const tracker = getNoopSymbolTrackerWithResolver(context); const checker = context.program.getTypeChecker(); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ddcc08c48ee64..ca7e33e060ba0 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -31,7 +31,6 @@ import { flatMapIterator, forEachExternalModuleToImportFrom, formatting, - FutureSourceFile, getAllowSyntheticDefaultImports, getBaseFileName, getDefaultExportInfoWorker, @@ -47,13 +46,11 @@ import { getNodeId, getQuoteFromPreference, getQuotePreference, - getQuotePreferenceFromFile, getSourceFileOfNode, getSymbolId, getTokenAtPosition, getTypeKeywordOfTypeOnlyImport, getUniqueSymbolId, - hasJSFileExtension, hostGetCanonicalFileName, Identifier, ImportClause, @@ -64,6 +61,7 @@ import { ImportsNotUsedAsValues, insertImports, InternalSymbolName, + isExternalModule, isExternalModuleReference, isIdentifier, isIdentifierPart, @@ -200,7 +198,7 @@ export interface ImportAdder { } /** @internal */ -export function createImportAdder(sourceFile: SourceFile | FutureSourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { +export function createImportAdder(sourceFile: SourceFile, program: Program, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken?: CancellationToken): ImportAdder { return createImportAdderWorker(sourceFile, program, /*useAutoImportProvider*/ false, preferences, host, cancellationToken); } @@ -210,7 +208,7 @@ interface AddToExistingState { readonly namedImports: Map; } -function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { +function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder { const compilerOptions = program.getCompilerOptions(); // Namespace fixes don't conflict, so just build a list. const addToNamespace: FixUseNamespaceImport[] = []; @@ -234,7 +232,7 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions)); const checker = program.getTypeChecker(); const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker)); - const exportInfo = getAllExportInfoForSymbol(sourceFile.path, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); + const exportInfo = getAllExportInfoForSymbol(sourceFile, symbol, symbolName, moduleSymbol, /*preferCapitalized*/ false, program, host, preferences, cancellationToken); const useRequire = shouldUseRequire(sourceFile, program); const fix = getImportFixForSymbol(sourceFile, Debug.checkDefined(exportInfo), program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences); if (fix) { @@ -348,17 +346,14 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog } function writeFixes(changeTracker: textChanges.ChangeTracker) { - const quotePreference = sourceFile.kind ? getQuotePreferenceFromFile(sourceFile, preferences) : getQuotePreference(preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); for (const fix of addToNamespace) { - Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addNamespaceQualifier(changeTracker, sourceFile, fix); } for (const fix of importType) { - Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); addImportType(changeTracker, sourceFile, fix, quotePreference); } addToExisting.forEach(({ importClauseOrBindingPattern, defaultImport, namedImports }) => { - Debug.assert(sourceFile.kind, "Cannot add to an existing import in a non-existent file."); doAddExistingFix( changeTracker, sourceFile, @@ -514,14 +509,14 @@ export function getImportCompletionAction( if (exportMapKey) { // The new way: `exportMapKey` should be in the `data` of each auto-import completion entry and // sent back when asking for details. - exportInfos = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); + exportInfos = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken).get(sourceFile.path, exportMapKey); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified exportMapKey"); } else { // The old way, kept alive for super old editors that don't give us `data` back. exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name)) ? [getSingleExportInfoForSymbol(targetSymbol, symbolName, moduleSymbol, program, host)] - : getAllExportInfoForSymbol(sourceFile.path, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); + : getAllExportInfoForSymbol(sourceFile, targetSymbol, symbolName, moduleSymbol, isJsxTagName, program, host, preferences, cancellationToken); Debug.assertIsDefined(exportInfos, "Some exportInfo should match the specified symbol / moduleSymbol"); } @@ -550,7 +545,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences)); } -function getImportFixForSymbol(sourceFile: SourceFile | FutureSourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { +function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], program: Program, position: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { const packageJsonImportFilter = createPackageJsonImportFilter(sourceFile, preferences, host); return getBestFix(getImportFixes(exportInfos, position, isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences).fixes, sourceFile, program, packageJsonImportFilter, host); } @@ -559,10 +554,10 @@ function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAc return { description, changes, commands }; } -function getAllExportInfoForSymbol(importingFilePath: Path, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { +function getAllExportInfoForSymbol(importingFile: SourceFile, symbol: Symbol, symbolName: string, moduleSymbol: Symbol, preferCapitalized: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): readonly SymbolExportInfo[] | undefined { const getChecker = createGetChecker(program, host); - return getExportInfoMap(importingFilePath, host, program, preferences, cancellationToken) - .search(importingFilePath, preferCapitalized, name => name === symbolName, info => { + return getExportInfoMap(importingFile, host, program, preferences, cancellationToken) + .search(importingFile.path, preferCapitalized, name => name === symbolName, info => { if (skipAlias(info[0].symbol, getChecker(info[0].isFromPackageJson)) === symbol && info.some(i => i.moduleSymbol === moduleSymbol || i.symbol.parent === moduleSymbol)) { return info; } @@ -596,16 +591,16 @@ function getImportFixes( isValidTypeOnlyUseSite: boolean, useRequire: boolean, program: Program, - sourceFile: SourceFile | FutureSourceFile, + sourceFile: SourceFile, host: LanguageServiceHost, preferences: UserPreferences, importMap = sourceFile.kind && createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly ImportFixWithModuleSpecifier[] } { const checker = program.getTypeChecker(); - const existingImports = importMap && flatMap(exportInfos, importMap.getImportsForExportInfo); - const useNamespace = existingImports && usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); - const addToExisting = existingImports && tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); + const existingImports = flatMap(exportInfos, importMap.getImportsForExportInfo); + const useNamespace = usagePosition !== undefined && tryUseExistingNamespaceImport(existingImports, usagePosition); + const addToExisting = tryAddToExistingImport(existingImports, isValidTypeOnlyUseSite, checker, program.getCompilerOptions()); if (addToExisting) { // Don't bother providing an action to add a new import if we can add to an existing one. return { @@ -792,9 +787,9 @@ function createExistingImportMap(checker: TypeChecker, importingFile: SourceFile }; } -function shouldUseRequire(sourceFile: SourceFile | FutureSourceFile, program: Program): boolean { +function shouldUseRequire(sourceFile: SourceFile, program: Program): boolean { // 1. TypeScript files don't use require variable declarations - if (sourceFile.kind && !isSourceFileJS(sourceFile) || !sourceFile.kind && !hasJSFileExtension(sourceFile.fileName)) { + if (!isSourceFileJS(sourceFile)) { return false; } @@ -825,7 +820,7 @@ function createGetChecker(program: Program, host: LanguageServiceHost) { function getNewImportFixes( program: Program, - sourceFile: SourceFile | FutureSourceFile, + sourceFile: SourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -834,14 +829,14 @@ function getNewImportFixes( preferences: UserPreferences, fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixAddJsdocTypeImport)[] } { - const isJs = sourceFile.kind ? isSourceFileJS(sourceFile) : hasJSFileExtension(sourceFile.fileName); + const isJs = isSourceFileJS(sourceFile); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); const getChecker = createGetChecker(program, host); const moduleResolution = getEmitModuleResolutionKind(compilerOptions); const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(moduleResolution); const getModuleSpecifiers = fromCacheOnly - ? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile.path, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) + ? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false }) : (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences); let computedWithoutCacheCount = 0; @@ -897,9 +892,9 @@ function getNewImportFixes( function getFixesForAddImport( exportInfos: readonly SymbolExportInfo[], - existingImports: readonly FixAddToExistingImportInfo[] | undefined, + existingImports: readonly FixAddToExistingImportInfo[], program: Program, - sourceFile: SourceFile | FutureSourceFile, + sourceFile: SourceFile, usagePosition: number | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, @@ -963,7 +958,7 @@ function sortFixInfo(fixes: readonly (FixInfo & { fix: ImportFixWithModuleSpecif compareModuleSpecifiers(a.fix, b.fix, sourceFile, program, packageJsonImportFilter.allowsImportingSpecifier, _toPath)); } -function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile | FutureSourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { +function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: SourceFile, program: Program, packageJsonImportFilter: PackageJsonImportFilter, host: LanguageServiceHost): ImportFixWithModuleSpecifier | undefined { if (!some(fixes)) return; // These will always be placed first if available, and are better than other kinds if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) { @@ -987,7 +982,7 @@ function getBestFix(fixes: readonly ImportFixWithModuleSpecifier[], sourceFile: function compareModuleSpecifiers( a: ImportFixWithModuleSpecifier, b: ImportFixWithModuleSpecifier, - importingFile: SourceFile | FutureSourceFile, + importingFile: SourceFile, program: Program, allowsImportingSpecifier: (specifier: string) => boolean, toPath: (fileName: string) => Path, @@ -1008,7 +1003,7 @@ function compareModuleSpecifiers( // This can produce false positives or negatives if re-exports cross into sibling directories // (e.g. `export * from "../whatever"`) or are not named "index" (we don't even try to consider // this if we're in a resolution mode where you can't drop trailing "/index" from paths). -function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { +function isFixPossiblyReExportingImportingFile(fix: ImportFixWithModuleSpecifier, importingFile: SourceFile, compilerOptions: CompilerOptions, toPath: (fileName: string) => Path): boolean { if (fix.isReExport && fix.exportInfo?.moduleFileName && getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node10 && @@ -1024,7 +1019,7 @@ function isIndexFileName(fileName: string) { return getBaseFileName(fileName, [".js", ".jsx", ".d.ts", ".ts", ".tsx"], /*ignoreCase*/ true) === "index"; } -function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile | FutureSourceFile, program: Program): Comparison { +function compareNodeCoreModuleSpecifiers(a: string, b: string, importingFile: SourceFile, program: Program): Comparison { if (startsWith(a, "node:") && !startsWith(b, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.LessThan : Comparison.GreaterThan; if (startsWith(b, "node:") && !startsWith(a, "node:")) return shouldUseUriStyleNodeCoreModules(importingFile, program) ? Comparison.GreaterThan : Comparison.LessThan; return Comparison.EqualTo; @@ -1069,7 +1064,7 @@ function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { * * @internal */ -export function getImportKind(importingFile: SourceFile | FutureSourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { +export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions, forceImportKeyword?: boolean): ImportKind { if (compilerOptions.verbatimModuleSyntax && (getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS || importingFile.impliedNodeFormat === ModuleKind.CommonJS)) { // TODO: if the exporting file is ESM under nodenext, or `forceImport` is given in a JS file, this is impossible return ImportKind.CommonJS; @@ -1083,7 +1078,7 @@ export function getImportKind(importingFile: SourceFile | FutureSourceFile, expo } } -function getUmdImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { // Import a synthetic `default` if enabled. if (getAllowSyntheticDefaultImports(compilerOptions)) { return ImportKind.Default; @@ -1095,8 +1090,8 @@ function getUmdImportKind(importingFile: SourceFile | FutureSourceFile, compiler case ModuleKind.AMD: case ModuleKind.CommonJS: case ModuleKind.UMD: - if (importingFile.kind ? isInJSFile(importingFile) : hasJSFileExtension(importingFile.fileName)) { - return importingFile.externalModuleIndicator || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; + if (isInJSFile(importingFile)) { + return isExternalModule(importingFile) || forceImportKeyword ? ImportKind.Namespace : ImportKind.CommonJS; } return ImportKind.CommonJS; case ModuleKind.System: @@ -1211,9 +1206,9 @@ function getExportInfos( return originalSymbolToExportInfos; } -function getExportEqualsImportKind(importingFile: SourceFile | FutureSourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { +function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions, forceImportKeyword: boolean): ImportKind { const allowSyntheticDefaults = getAllowSyntheticDefaultImports(compilerOptions); - const isJS = importingFile.kind ? isInJSFile(importingFile) : hasJSFileExtension(importingFile.fileName); + const isJS = isInJSFile(importingFile); // 1. 'import =' will not work in es2015+ TS files, so the decision is between a default // and a namespace import, based on allowSyntheticDefaultImports/esModuleInterop. if (!isJS && getEmitModuleKind(compilerOptions) >= ModuleKind.ES2015) { @@ -1222,19 +1217,17 @@ function getExportEqualsImportKind(importingFile: SourceFile | FutureSourceFile, // 2. 'import =' will not work in JavaScript, so the decision is between a default import, // a namespace import, and const/require. if (isJS) { - return importingFile.externalModuleIndicator || forceImportKeyword + return isExternalModule(importingFile) || forceImportKeyword ? allowSyntheticDefaults ? ImportKind.Default : ImportKind.Namespace : ImportKind.CommonJS; } // 3. At this point the most correct choice is probably 'import =', but people // really hate that, so look to see if the importing file has any precedent // on how to handle it. - if (importingFile.kind) { - for (const statement of importingFile.statements) { - // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration - if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { - return ImportKind.CommonJS; - } + for (const statement of importingFile.statements) { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { + return ImportKind.CommonJS; } } // 4. We have no precedent to go on, so just use a default import if @@ -1250,7 +1243,7 @@ function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: S return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports); } function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): DiagnosticOrDiagnosticAndArguments { - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); switch (fix.kind) { case ImportFixKind.UseNamespace: addNamespaceQualifier(changes, sourceFile, fix); diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 5972b29ab1a1a..75f72964530b6 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -2,7 +2,7 @@ import { AnyImportSyntax, Diagnostics, Expression, - getQuotePreferenceFromFile, + getQuotePreference, getTokenAtPosition, Identifier, isExternalModuleReference, @@ -57,5 +57,5 @@ function getInfo(sourceFile: SourceFile, pos: number): Info | undefined { } function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info, preferences: UserPreferences): void { - changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreferenceFromFile(sourceFile, preferences))); + changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreference(sourceFile, preferences))); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 01fa6ea982a59..43ae8c2497a1c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -96,7 +96,7 @@ import { getNewLineKind, getNewLineOrDefaultFromHost, getPropertyNameForPropertyNameNode, - getQuotePreferenceFromFile, + getQuotePreference, getReplacementSpanForContextToken, getRootDeclaration, getSourceFileOfModule, @@ -790,7 +790,7 @@ function continuePreviousIncompleteResponse( const touchNode = getTouchingPropertyName(file, position); const lowerCaseTokenText = location.text.toLowerCase(); - const exportMap = getExportInfoMap(file.path, host, program, preferences, cancellationToken); + const exportMap = getExportInfoMap(file, host, program, preferences, cancellationToken); const newEntries = resolvingModuleSpecifiers( "continuePreviousIncompleteResponse", host, @@ -1105,7 +1105,7 @@ function getJSDocParamAnnotation( const inferredType = checker.getTypeAtLocation(initializer.parent); if (!(inferredType.flags & (TypeFlags.Any | TypeFlags.Void))) { const sourceFile = initializer.getSourceFile(); - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const builderFlags = (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); const typeNode = checker.typeToTypeNode(inferredType, findAncestor(initializer, isFunctionLike), builderFlags); if (typeNode) { @@ -1353,7 +1353,7 @@ function getExhaustiveCaseSnippets( const tracker = newCaseClauseTracker(checker, clauses); const target = getEmitScriptTarget(options); - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host); const elements: Expression[] = []; for (const type of switchType.types as LiteralType[]) { @@ -2048,7 +2048,7 @@ function createObjectLiteralMethod( const declaration = declarations[0]; const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName; const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration)); - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const builderFlags = NodeBuilderFlags.OmitThisParameter | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : NodeBuilderFlags.None); switch (declaration.kind) { @@ -3747,7 +3747,7 @@ function getCompletionData( ""; const moduleSpecifierCache = host.getModuleSpecifierCache?.(); - const exportInfo = getExportInfoMap(sourceFile.path, host, program, preferences, cancellationToken); + const exportInfo = getExportInfoMap(sourceFile, host, program, preferences, cancellationToken); const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host); resolvingModuleSpecifiers( diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index d1029858b0a45..4651c1dac1c36 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -451,7 +451,7 @@ function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly So } /** @internal */ -export function getExportInfoMap(importingFilePath: Path, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { +export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, preferences: UserPreferences, cancellationToken: CancellationToken | undefined): ExportInfoMap { const start = timestamp(); // Pulling the AutoImportProvider project will trigger its updateGraph if pending, // which will invalidate the export map cache if things change, so pull it before @@ -463,7 +463,7 @@ export function getExportInfoMap(importingFilePath: Path, host: LanguageServiceH getGlobalTypingsCacheLocation: () => host.getGlobalTypingsCacheLocation?.(), }); - if (cache.isUsableByFile(importingFilePath)) { + if (cache.isUsableByFile(importingFile.path)) { host.log?.("getExportInfoMap: cache hit"); return cache; } @@ -481,7 +481,7 @@ export function getExportInfoMap(importingFilePath: Path, host: LanguageServiceH // can cause it to happen: see 'completionsImport_mergedReExport.ts' if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) { cache.add( - importingFilePath, + importingFile.path, defaultInfo.symbol, defaultInfo.exportKind === ExportKind.Default ? InternalSymbolName.Default : InternalSymbolName.ExportEquals, moduleSymbol, @@ -493,7 +493,7 @@ export function getExportInfoMap(importingFilePath: Path, host: LanguageServiceH checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => { if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, key)) { cache.add( - importingFilePath, + importingFile.path, exported, key, moduleSymbol, diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 3330d1d0adb86..1212a7b7d7d01 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -7,7 +7,7 @@ import { Diagnostics, emptyArray, getLocaleSpecificMessage, - getQuotePreferenceFromFile, + getQuotePreference, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, @@ -92,7 +92,7 @@ function getNewStatementsAndRemoveFromOldFile( } const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreferenceFromFile(oldFile, preferences); + const quotePreference = getQuotePreference(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 9a9bbbf30143f..cfd1ba19ba1ef 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -6,7 +6,7 @@ import { emptyArray, getBaseFileName, getLocaleSpecificMessage, - getQuotePreferenceFromFile, + getQuotePreference, hasSyntacticModifier, hostGetCanonicalFileName, Identifier, @@ -82,7 +82,7 @@ function getNewStatementsAndRemoveFromOldFile( } const useEsModuleSyntax = !!oldFile.externalModuleIndicator; - const quotePreference = getQuotePreferenceFromFile(oldFile, preferences); + const quotePreference = getQuotePreference(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 493955b2a2f08..aac18cce972ed 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -41,7 +41,6 @@ import { formatting, FunctionDeclaration, FunctionExpression, - FutureSourceFile, getAncestor, getFirstNonSpaceCharacterPosition, getFormatCodeSettingsForWriting, @@ -608,35 +607,25 @@ export class ChangeTracker { this.replaceRangeWithNodes(sourceFile, createRange(pos), newNodes, options); } - public insertNodeAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNode: Statement, blankLineBetween: boolean): void { + public insertNodeAtTopOfFile(sourceFile: SourceFile, newNode: Statement, blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNode, blankLineBetween); } - public insertNodesAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { + public insertNodesAtTopOfFile(sourceFile: SourceFile, newNodes: readonly Statement[], blankLineBetween: boolean): void { this.insertAtTopOfFile(sourceFile, newNodes, blankLineBetween); } - private insertAtTopOfFile(sourceFile: SourceFile | FutureSourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { - const pos = sourceFile.kind ? getInsertionPositionAtSourceFileTop(sourceFile) : 0; + private insertAtTopOfFile(sourceFile: SourceFile, insert: Statement | readonly Statement[], blankLineBetween: boolean): void { + const pos = getInsertionPositionAtSourceFileTop(sourceFile); const options = { prefix: pos === 0 ? undefined : this.newLineCharacter, - suffix: (sourceFile.kind && isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), + suffix: (isLineBreak(sourceFile.text.charCodeAt(pos)) ? "" : this.newLineCharacter) + (blankLineBetween ? this.newLineCharacter : ""), }; if (isArray(insert)) { - if (sourceFile.kind) { - this.insertNodesAt(sourceFile, pos, insert, options); - } - else { - this.insertStatementsInNewFile(sourceFile.fileName, insert); - } + this.insertNodesAt(sourceFile, pos, insert, options); } else { - if (sourceFile.kind) { - this.insertNodeAt(sourceFile, pos, insert, options); - } - else { - this.insertStatementsInNewFile(sourceFile.fileName, [insert]); - } + this.insertNodeAt(sourceFile, pos, insert, options); } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index b1bcb74cfd2ec..e96f1c603556e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -85,13 +85,11 @@ import { FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, - FutureSourceFile, getAssignmentDeclarationKind, getCombinedNodeFlagsAlwaysIncludeJSDoc, getDirectoryPath, getEmitScriptTarget, getExternalModuleImportEqualsDeclarationExpression, - getImpliedNodeFormatForFile, getIndentString, getJSDocEnumTag, getLastChild, @@ -269,8 +267,6 @@ import { ModifierFlags, ModuleDeclaration, ModuleInstanceState, - ModuleKind, - ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierResolutionHost, moduleSpecifiers, @@ -354,7 +350,6 @@ import { textSpanEnd, Token, tokenToString, - toPath, tryCast, Type, TypeChecker, @@ -2477,14 +2472,9 @@ export function quotePreferenceFromString(str: StringLiteral, sourceFile: Source } /** @internal */ -export function getQuotePreference(preferences: UserPreferences): QuotePreference { - return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; -} - -/** @internal */ -export function getQuotePreferenceFromFile(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { +export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference { if (preferences.quotePreference && preferences.quotePreference !== "auto") { - return getQuotePreference(preferences); + return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double; } else { // ignore synthetic import added when importHelpers: true @@ -2571,18 +2561,17 @@ export function findModifier(node: Node, kind: Modifier["kind"]): Modifier | und } /** @internal */ -export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile | FutureSourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { +export function insertImports(changes: textChanges.ChangeTracker, sourceFile: SourceFile, imports: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[], blankLineBetween: boolean, preferences: UserPreferences): void { const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; - const existingImportStatements = sourceFile.kind ? filter(sourceFile.statements, importKindPredicate) : undefined; + const existingImportStatements = filter(sourceFile.statements, importKindPredicate); let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; - if (!existingImportStatements?.length) { + if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { - Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); @@ -2601,7 +2590,6 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So else { const lastExistingImport = lastOrUndefined(existingImportStatements); if (lastExistingImport) { - Debug.assert(sourceFile.kind, "Cannot have existing import statements in a non-existent source file."); changes.insertNodesAfter(sourceFile, lastExistingImport, sortedNewImports); } else { @@ -3339,7 +3327,7 @@ export function getContextualTypeFromParent(node: Expression, checker: TypeCheck /** @internal */ export function quote(sourceFile: SourceFile, preferences: UserPreferences, text: string): string { // Editors can pass in undefined or empty string - we want to infer the preference in those cases. - const quotePreference = getQuotePreferenceFromFile(sourceFile, preferences); + const quotePreference = getQuotePreference(sourceFile, preferences); const quoted = JSON.stringify(text); return quotePreference === QuotePreference.Single ? `'${stripQuotes(quoted).replace(/'/g, "\\'").replace(/\\"/g, '"')}'` : quoted; } @@ -3700,7 +3688,7 @@ export interface PackageJsonImportFilter { } /** @internal */ -export function createPackageJsonImportFilter(fromFile: SourceFile | FutureSourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { +export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter { const packageJsons = ( (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) ).filter(p => p.parseable); @@ -3800,7 +3788,7 @@ export function createPackageJsonImportFilter(fromFile: SourceFile | FutureSourc // from Node core modules or not. We can start by seeing if the user is actually using // any node core modules, as opposed to simply having @types/node accidentally as a // dependency of a dependency. - if (fromFile.kind && isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { + if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { if (usesNodeCoreModules === undefined) { usesNodeCoreModules = consumesNodeCoreModules(fromFile); } @@ -4058,8 +4046,8 @@ export function isDeprecatedDeclaration(decl: Declaration) { } /** @internal */ -export function shouldUseUriStyleNodeCoreModules(file: SourceFile | FutureSourceFile, program: Program): boolean { - const decisionFromFile = file.kind && firstDefined(file.imports, node => { +export function shouldUseUriStyleNodeCoreModules(file: SourceFile, program: Program): boolean { + const decisionFromFile = firstDefined(file.imports, node => { if (JsTyping.nodeCoreModules.has(node.text)) { return startsWith(node.text, "node:"); } @@ -4081,27 +4069,6 @@ export function diagnosticToString(diag: DiagnosticOrDiagnosticAndArguments): st : getLocaleSpecificMessage(diag); } -/** @internal */ -export function createFutureSourceFile( - fileName: string, - program: Program, - host: ModuleResolutionHost, - moduleSyntax?: ModuleKind.CommonJS | ModuleKind.ESNext, -): FutureSourceFile { - const path = toPath(fileName, /*basePath*/ undefined, program.getCanonicalFileName); - const impliedNodeFormat = getImpliedNodeFormatForFile(path, program.getPackageJsonInfoCache?.(), host, program.getCompilerOptions()); - if (moduleSyntax === ModuleKind.CommonJS && impliedNodeFormat === ModuleKind.ESNext) { - Debug.fail("ES Modules cannot contain CommonJS syntax"); - } - return { - fileName, - path, - commonJsModuleIndicator: moduleSyntax === ModuleKind.CommonJS, - externalModuleIndicator: moduleSyntax === ModuleKind.ESNext, - impliedNodeFormat, - }; -} - /** * Get format code settings for a code writing context (e.g. when formatting text changes or completions code). * diff --git a/tests/cases/fourslash/moveToFile_ctsTomts.ts b/tests/cases/fourslash/moveToFile_ctsTomts.ts index 250ddcbe3c9b4..d2c057270dafd 100644 --- a/tests/cases/fourslash/moveToFile_ctsTomts.ts +++ b/tests/cases/fourslash/moveToFile_ctsTomts.ts @@ -20,7 +20,7 @@ verify.moveToFile({ newFileContents: { "/src/dir1/a.cts": -`import { y } from '../dir2/bar.mts'; +`import { y } from '../dir2/bar.mjs'; export const p = 0; y;`, diff --git a/tests/cases/fourslash/moveToFile_typeImport2.ts b/tests/cases/fourslash/moveToFile_typeImport2.ts index cf38d2e24a855..1850a3786730e 100644 --- a/tests/cases/fourslash/moveToFile_typeImport2.ts +++ b/tests/cases/fourslash/moveToFile_typeImport2.ts @@ -1,4 +1,3 @@ - /// // @Filename: /bar.ts From 6068ec4d72221f08dad0d2e8eb6dc5f30a6a1498 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 10 Apr 2023 00:53:37 -0700 Subject: [PATCH 28/42] Accepting baselines --- .../skips-lib.d.ts-files.js | 50 ++++++------- ...ggests-only-.js-file-for-a-.js-filepath.js | 52 ++++++------- ...ggests-only-.ts-file-for-a-.ts-filepath.js | 56 +++++++------- ...excluding-node_modules-within-a-project.js | 66 ++++++++--------- ...es-moving-statement-to-an-existing-file.js | 74 +++++++++++-------- 5 files changed, 157 insertions(+), 141 deletions(-) diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js index 3a28e6b0a1983..c8936ee373e3d 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js @@ -1,5 +1,5 @@ currentDirectory:: / useCaseSensitiveFileNames: false -Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist Before request //// [/file1.d.ts] class C {} @@ -17,7 +17,7 @@ class C {} {"files":["./file1.d.ts","./a/lib.d.ts","./a/file3.d.ts","/a/lib.es6.d.ts"]} -Info 1 [00:00:16.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "open", "arguments": { @@ -26,11 +26,11 @@ Info 1 [00:00:16.000] request: "seq": 1, "type": "request" } -Info 2 [00:00:17.000] Search path: / -Info 3 [00:00:18.000] For info: /file1.d.ts :: Config file name: /tsconfig.json -Info 4 [00:00:19.000] Creating configuration project /tsconfig.json -Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file -Info 6 [00:00:21.000] Config: /tsconfig.json : { +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.d.ts :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { "rootNames": [ "/file1.d.ts", "/a/lib.d.ts", @@ -41,14 +41,14 @@ Info 6 [00:00:21.000] Config: /tsconfig.json : { "configFilePath": "/tsconfig.json" } } -Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /a/lib.d.ts 500 undefined WatchType: Closed Script info -Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /a/file3.d.ts 500 undefined WatchType: Closed Script info -Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /a/lib.es6.d.ts 500 undefined WatchType: Closed Script info -Info 10 [00:00:25.000] Starting updateGraphWorker: Project: /tsconfig.json -Info 11 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file -Info 12 [00:00:27.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 13 [00:00:28.000] Project '/tsconfig.json' (Configured) -Info 14 [00:00:29.000] Files (4) +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/file3.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib.es6.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) /file1.d.ts SVC-1-0 "class C {}" /a/lib.d.ts Text-1 "" /a/file3.d.ts Text-1 "" @@ -64,15 +64,15 @@ Info 14 [00:00:29.000] Files (4) a/lib.es6.d.ts Part of 'files' list in tsconfig.json -Info 15 [00:00:30.000] ----------------------------------------------- -Info 16 [00:00:31.000] Project '/tsconfig.json' (Configured) -Info 16 [00:00:32.000] Files (4) +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (4) -Info 16 [00:00:33.000] ----------------------------------------------- -Info 16 [00:00:34.000] Open files: -Info 16 [00:00:35.000] FileName: /file1.d.ts ProjectRootPath: undefined -Info 16 [00:00:36.000] Projects: /tsconfig.json -Info 16 [00:00:37.000] response: +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.d.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: { "responseRequired": false } @@ -94,7 +94,7 @@ FsWatches:: Before request -Info 17 [00:00:38.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "getMoveToRefactoringFileSuggestions", "arguments": { @@ -105,7 +105,7 @@ Info 17 [00:00:38.000] request: "seq": 2, "type": "request" } -Info 18 [00:00:39.000] response: +Info seq [hh:mm:ss:mss] response: { "response": { "newFilename": "/C.d.ts", diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js index 6d5f7abb92a04..ceaa602719133 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js @@ -1,5 +1,5 @@ currentDirectory:: / useCaseSensitiveFileNames: false -Info 0 [00:00:15.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist Before request //// [/file1.js] class C {} @@ -20,7 +20,7 @@ class C {} {"files":["./file1.js","./file2.js","./file3.mts","./file4.ts","./file5.js"]} -Info 1 [00:00:16.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "open", "arguments": { @@ -29,11 +29,11 @@ Info 1 [00:00:16.000] request: "seq": 1, "type": "request" } -Info 2 [00:00:17.000] Search path: / -Info 3 [00:00:18.000] For info: /file1.js :: Config file name: /tsconfig.json -Info 4 [00:00:19.000] Creating configuration project /tsconfig.json -Info 5 [00:00:20.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file -Info 6 [00:00:21.000] Config: /tsconfig.json : { +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.js :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { "rootNames": [ "/file1.js", "/file2.js", @@ -45,15 +45,15 @@ Info 6 [00:00:21.000] Config: /tsconfig.json : { "configFilePath": "/tsconfig.json" } } -Info 7 [00:00:22.000] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info -Info 8 [00:00:23.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info -Info 9 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info -Info 10 [00:00:25.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info -Info 11 [00:00:26.000] Starting updateGraphWorker: Project: /tsconfig.json -Info 12 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file -Info 13 [00:00:28.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 14 [00:00:29.000] Project '/tsconfig.json' (Configured) -Info 15 [00:00:30.000] Files (5) +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file2.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file4.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (5) /file1.js SVC-1-0 "class C {}" /file2.js Text-1 "" /file3.mts Text-1 "" @@ -72,15 +72,15 @@ Info 15 [00:00:30.000] Files (5) file5.js Part of 'files' list in tsconfig.json -Info 16 [00:00:31.000] ----------------------------------------------- -Info 17 [00:00:32.000] Project '/tsconfig.json' (Configured) -Info 17 [00:00:33.000] Files (5) +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (5) -Info 17 [00:00:34.000] ----------------------------------------------- -Info 17 [00:00:35.000] Open files: -Info 17 [00:00:36.000] FileName: /file1.js ProjectRootPath: undefined -Info 17 [00:00:37.000] Projects: /tsconfig.json -Info 17 [00:00:38.000] response: +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.js ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: { "responseRequired": false } @@ -104,7 +104,7 @@ FsWatches:: Before request -Info 18 [00:00:39.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "getMoveToRefactoringFileSuggestions", "arguments": { @@ -115,7 +115,7 @@ Info 18 [00:00:39.000] request: "seq": 2, "type": "request" } -Info 19 [00:00:40.000] response: +Info seq [hh:mm:ss:mss] response: { "response": { "newFilename": "/C.js", diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js index 8e32072c58c41..ced34f1ef2725 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js @@ -1,5 +1,5 @@ currentDirectory:: / useCaseSensitiveFileNames: false -Info 0 [00:00:19.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist Before request //// [/file1.ts] interface ka { @@ -29,7 +29,7 @@ interface ka { {"files":["./file1.ts","./file2.tsx","./file3.mts","./file4.cts","./file5.js","./file6.d.ts","./file7.ts"]} -Info 1 [00:00:20.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "open", "arguments": { @@ -38,11 +38,11 @@ Info 1 [00:00:20.000] request: "seq": 1, "type": "request" } -Info 2 [00:00:21.000] Search path: / -Info 3 [00:00:22.000] For info: /file1.ts :: Config file name: /tsconfig.json -Info 4 [00:00:23.000] Creating configuration project /tsconfig.json -Info 5 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file -Info 6 [00:00:25.000] Config: /tsconfig.json : { +Info seq [hh:mm:ss:mss] Search path: / +Info seq [hh:mm:ss:mss] For info: /file1.ts :: Config file name: /tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /tsconfig.json 2000 undefined Project: /tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /tsconfig.json : { "rootNames": [ "/file1.ts", "/file2.tsx", @@ -56,17 +56,17 @@ Info 6 [00:00:25.000] Config: /tsconfig.json : { "configFilePath": "/tsconfig.json" } } -Info 7 [00:00:26.000] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info -Info 8 [00:00:27.000] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info -Info 9 [00:00:28.000] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info -Info 10 [00:00:29.000] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info -Info 11 [00:00:30.000] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info -Info 12 [00:00:31.000] FileWatcher:: Added:: WatchInfo: /file7.ts 500 undefined WatchType: Closed Script info -Info 13 [00:00:32.000] Starting updateGraphWorker: Project: /tsconfig.json -Info 14 [00:00:33.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file -Info 15 [00:00:34.000] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 16 [00:00:35.000] Project '/tsconfig.json' (Configured) -Info 17 [00:00:36.000] Files (7) +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file2.tsx 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file3.mts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file4.cts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file5.js 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file6.d.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /file7.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (7) /file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " /file2.tsx Text-1 "" /file3.mts Text-1 "" @@ -91,15 +91,15 @@ Info 17 [00:00:36.000] Files (7) file7.ts Part of 'files' list in tsconfig.json -Info 18 [00:00:37.000] ----------------------------------------------- -Info 19 [00:00:38.000] Project '/tsconfig.json' (Configured) -Info 19 [00:00:39.000] Files (7) +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (7) -Info 19 [00:00:40.000] ----------------------------------------------- -Info 19 [00:00:41.000] Open files: -Info 19 [00:00:42.000] FileName: /file1.ts ProjectRootPath: undefined -Info 19 [00:00:43.000] Projects: /tsconfig.json -Info 19 [00:00:44.000] response: +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /file1.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /tsconfig.json +Info seq [hh:mm:ss:mss] response: { "responseRequired": false } @@ -127,7 +127,7 @@ FsWatches:: Before request -Info 20 [00:00:45.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "getMoveToRefactoringFileSuggestions", "arguments": { @@ -138,7 +138,7 @@ Info 20 [00:00:45.000] request: "seq": 2, "type": "request" } -Info 21 [00:00:46.000] response: +Info seq [hh:mm:ss:mss] response: { "response": { "newFilename": "/ka.ts", diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js index fbb83cef9af39..68bd5322cf7d0 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -1,5 +1,5 @@ currentDirectory:: / useCaseSensitiveFileNames: false -Info 0 [00:00:36.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist Before request //// [/project/a/file1.ts] interface ka { @@ -27,7 +27,7 @@ export const value1 = 0; {} -Info 1 [00:00:37.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "open", "arguments": { @@ -36,11 +36,11 @@ Info 1 [00:00:37.000] request: "seq": 1, "type": "request" } -Info 2 [00:00:38.000] Search path: /project/a -Info 3 [00:00:39.000] For info: /project/a/file1.ts :: Config file name: /project/tsconfig.json -Info 4 [00:00:40.000] Creating configuration project /project/tsconfig.json -Info 5 [00:00:41.000] FileWatcher:: Added:: WatchInfo: /project/tsconfig.json 2000 undefined Project: /project/tsconfig.json WatchType: Config file -Info 6 [00:00:42.000] Config: /project/tsconfig.json : { +Info seq [hh:mm:ss:mss] Search path: /project/a +Info seq [hh:mm:ss:mss] For info: /project/a/file1.ts :: Config file name: /project/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /project/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/tsconfig.json 2000 undefined Project: /project/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /project/tsconfig.json : { "rootNames": [ "/project/a/file1.ts", "/project/a/file4.ts", @@ -51,22 +51,22 @@ Info 6 [00:00:42.000] Config: /project/tsconfig.json : { "configFilePath": "/project/tsconfig.json" } } -Info 7 [00:00:43.000] DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory -Info 8 [00:00:44.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory -Info 9 [00:00:45.000] FileWatcher:: Added:: WatchInfo: /project/a/file4.ts 500 undefined WatchType: Closed Script info -Info 10 [00:00:46.000] FileWatcher:: Added:: WatchInfo: /project/b/file2.ts 500 undefined WatchType: Closed Script info -Info 11 [00:00:47.000] FileWatcher:: Added:: WatchInfo: /project/d/e/file3.ts 500 undefined WatchType: Closed Script info -Info 12 [00:00:48.000] Starting updateGraphWorker: Project: /project/tsconfig.json -Info 13 [00:00:49.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations -Info 14 [00:00:50.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations -Info 15 [00:00:51.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache -Info 16 [00:00:52.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache -Info 17 [00:00:53.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /project/tsconfig.json WatchType: Missing file -Info 18 [00:00:54.000] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots -Info 19 [00:00:55.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots -Info 20 [00:00:56.000] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 21 [00:00:57.000] Project '/project/tsconfig.json' (Configured) -Info 22 [00:00:58.000] Files (6) +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project 1 undefined Config: /project/tsconfig.json WatchType: Wild card directory +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/a/file4.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/b/file2.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/d/e/file3.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /project/tsconfig.json +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /project/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (6) /project/a/file1.ts SVC-1-0 "interface ka {\n name: string;\n }\n " /project/node_modules/@types/node/someFile.d.ts Text-1 "export const value = 0;" /project/node_modules/.cache/someFile.d.ts Text-1 "export const value1 = 0;" @@ -88,15 +88,15 @@ Info 22 [00:00:58.000] Files (6) d/e/file3.ts Matched by default include pattern '**/*' -Info 23 [00:00:59.000] ----------------------------------------------- -Info 24 [00:01:00.000] Project '/project/tsconfig.json' (Configured) -Info 24 [00:01:01.000] Files (6) +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/project/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (6) -Info 24 [00:01:02.000] ----------------------------------------------- -Info 24 [00:01:03.000] Open files: -Info 24 [00:01:04.000] FileName: /project/a/file1.ts ProjectRootPath: undefined -Info 24 [00:01:05.000] Projects: /project/tsconfig.json -Info 24 [00:01:06.000] response: +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /project/a/file1.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /project/tsconfig.json +Info seq [hh:mm:ss:mss] response: { "responseRequired": false } @@ -126,7 +126,7 @@ FsWatchesRecursive:: Before request -Info 25 [00:01:07.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "getMoveToRefactoringFileSuggestions", "arguments": { @@ -137,7 +137,7 @@ Info 25 [00:01:07.000] request: "seq": 2, "type": "request" } -Info 26 [00:01:08.000] response: +Info seq [hh:mm:ss:mss] response: { "response": { "newFilename": "/project/a/ka.ts", diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js index ef640e48f37fc..174ce6cec0d97 100644 --- a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -1,5 +1,5 @@ currentDirectory:: / useCaseSensitiveFileNames: false -Info 0 [00:00:11.000] Provided types map file "/a/lib/typesMap.json" doesn't exist +Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist Before request //// [/Foo/a.ts] const x = 0; @@ -12,7 +12,7 @@ const a = 1; { "files": ["./a.ts", "./b.ts"] } -Info 1 [00:00:12.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "open", "arguments": { @@ -21,11 +21,11 @@ Info 1 [00:00:12.000] request: "seq": 1, "type": "request" } -Info 2 [00:00:13.000] Search path: /Foo -Info 3 [00:00:14.000] For info: /Foo/a.ts :: Config file name: /Foo/tsconfig.json -Info 4 [00:00:15.000] Creating configuration project /Foo/tsconfig.json -Info 5 [00:00:16.000] FileWatcher:: Added:: WatchInfo: /Foo/tsconfig.json 2000 undefined Project: /Foo/tsconfig.json WatchType: Config file -Info 6 [00:00:17.000] Config: /Foo/tsconfig.json : { +Info seq [hh:mm:ss:mss] Search path: /Foo +Info seq [hh:mm:ss:mss] For info: /Foo/a.ts :: Config file name: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] Creating configuration project /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /Foo/tsconfig.json 2000 undefined Project: /Foo/tsconfig.json WatchType: Config file +Info seq [hh:mm:ss:mss] Config: /Foo/tsconfig.json : { "rootNames": [ "/Foo/a.ts", "/Foo/b.ts" @@ -34,18 +34,18 @@ Info 6 [00:00:17.000] Config: /Foo/tsconfig.json : { "configFilePath": "/Foo/tsconfig.json" } } -Info 7 [00:00:18.000] FileWatcher:: Added:: WatchInfo: /Foo/b.ts 500 undefined WatchType: Closed Script info -Info 8 [00:00:19.000] Starting updateGraphWorker: Project: /Foo/tsconfig.json -Info 9 [00:00:20.000] DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info 10 [00:00:21.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info 11 [00:00:22.000] DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info 12 [00:00:23.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info 13 [00:00:24.000] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /Foo/tsconfig.json WatchType: Missing file -Info 14 [00:00:25.000] DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots -Info 15 [00:00:26.000] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots -Info 16 [00:00:27.000] Finishing updateGraphWorker: Project: /Foo/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms -Info 17 [00:00:28.000] Project '/Foo/tsconfig.json' (Configured) -Info 18 [00:00:29.000] Files (2) +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /Foo/b.ts 500 undefined WatchType: Closed Script info +Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations +Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /Foo/tsconfig.json WatchType: Missing file +Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots +Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /Foo/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms +Info seq [hh:mm:ss:mss] Project '/Foo/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) /Foo/a.ts SVC-1-0 "const x = 0;" /Foo/b.ts Text-1 "import {} from \"./bar\";\nconst a = 1;" @@ -55,15 +55,15 @@ Info 18 [00:00:29.000] Files (2) b.ts Part of 'files' list in tsconfig.json -Info 19 [00:00:30.000] ----------------------------------------------- -Info 20 [00:00:31.000] Project '/Foo/tsconfig.json' (Configured) -Info 20 [00:00:32.000] Files (2) +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Project '/Foo/tsconfig.json' (Configured) +Info seq [hh:mm:ss:mss] Files (2) -Info 20 [00:00:33.000] ----------------------------------------------- -Info 20 [00:00:34.000] Open files: -Info 20 [00:00:35.000] FileName: /Foo/a.ts ProjectRootPath: undefined -Info 20 [00:00:36.000] Projects: /Foo/tsconfig.json -Info 20 [00:00:37.000] response: +Info seq [hh:mm:ss:mss] ----------------------------------------------- +Info seq [hh:mm:ss:mss] Open files: +Info seq [hh:mm:ss:mss] FileName: /Foo/a.ts ProjectRootPath: undefined +Info seq [hh:mm:ss:mss] Projects: /Foo/tsconfig.json +Info seq [hh:mm:ss:mss] response: { "responseRequired": false } @@ -87,7 +87,7 @@ FsWatches:: Before request -Info 21 [00:00:38.000] request: +Info seq [hh:mm:ss:mss] request: { "command": "getEditsForMoveToFileRefactor", "arguments": { @@ -103,7 +103,7 @@ Info 21 [00:00:38.000] request: "seq": 2, "type": "request" } -Info 22 [00:00:39.000] response: +Info seq [hh:mm:ss:mss] response: { "response": { "edits": [ @@ -122,6 +122,22 @@ Info 22 [00:00:39.000] response: "newText": "" } ] + }, + { + "fileName": "/Foo/b.ts", + "textChanges": [ + { + "start": { + "line": 2, + "offset": 13 + }, + "end": { + "line": 2, + "offset": 13 + }, + "newText": "\nconst x = 0;\n" + } + ] } ] }, From 9387f678f6b5e5abef834037d2d31b039d8aaa78 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 10 Apr 2023 12:23:54 -0700 Subject: [PATCH 29/42] FmoduleSpecifier fixes and cleanup Co-authored-by: Andrew Branch --- src/compiler/moduleSpecifiers.ts | 18 +++- src/harness/fourslashImpl.ts | 1 - src/services/moveToFileAndNewFile.ts | 97 ++++++------------- src/services/refactors/moveToFile.ts | 13 +-- src/services/refactors/moveToNewFile.ts | 9 +- tests/cases/fourslash/moveToFile_ctsTomts.ts | 2 +- ...ypeImport1.ts => moveToFile_typeImport.ts} | 6 +- .../cases/fourslash/moveToFile_typeImport2.ts | 27 ------ 8 files changed, 62 insertions(+), 111 deletions(-) rename tests/cases/fourslash/{moveToFile_typeImport1.ts => moveToFile_typeImport.ts} (78%) delete mode 100644 tests/cases/fourslash/moveToFile_typeImport2.ts diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 87efee364c0d9..c16abc9a809c7 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -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,9 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie return fileName; } - if (allowedEndings[0] === ModuleSpecifierEnding.TsExtension && fileExtensionIsOneOf(fileName, [Extension.Mts, 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])) { @@ -1075,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); diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 46f114aea81d0..45ae78dac5707 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3260,7 +3260,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)); //TODO: fix newText? const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges); this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent); } diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts index 001604e326c08..d84c9440be5f0 100644 --- a/src/services/moveToFileAndNewFile.ts +++ b/src/services/moveToFileAndNewFile.ts @@ -2,7 +2,7 @@ import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, ModuleKind, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; -import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, copyEntries, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachEntry,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSymbolId,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; +import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachKey,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isValidTypeOnlyAliasUseSite, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; /**@internal */ export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { @@ -28,7 +28,7 @@ export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly St } /**@internal */ -export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: ReadonlySymbolSet, checker: TypeChecker) { +export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: Set, 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)!))); @@ -37,7 +37,7 @@ export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Sta /**@internal */ export function updateImportsInOtherFiles( - changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: ReadonlySymbolSet, newFilename: string, quotePreference: QuotePreference + changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, newFilename: string, quotePreference: QuotePreference ): void { const checker = program.getTypeChecker(); for (const sourceFile of program.getSourceFiles()) { @@ -86,7 +86,7 @@ export function updateNamespaceLikeImport( changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, - movedSymbols: ReadonlySymbolSet, + movedSymbols: Set, newModuleSpecifier: string, oldImportId: Identifier, oldImportNode: SupportedImport, @@ -175,7 +175,7 @@ export type SupportedImportStatement = /**@internal */ export function createOldFileImportsFromNewFile( sourceFile: SourceFile, - newFileNeedExport: ReadonlySymbolSet, + newFileNeedExport: Set, newFileNameWithExtension: string, program: Program, host: LanguageServiceHost, @@ -227,7 +227,7 @@ function makeVariableStatement(name: BindingName, type: TypeNode | undefined, in } /**@internal */ -export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: ReadonlySymbolSet, useEs6Exports: boolean): readonly Statement[] { +export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Set, useEs6Exports: boolean): readonly Statement[] { return flatMap(toMove, statement => { if (isTopLevelDeclarationStatement(statement) && !isExported(sourceFile, statement, useEs6Exports) && @@ -490,27 +490,27 @@ export interface StatementRange { readonly afterLast: Statement | undefined; } -/**@internal */ -export interface ReadonlySymbolSet { - size(): number; - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -} +// /**@internal */ +// export interface Set { +// size(): number; +// has(symbol: Symbol): boolean; +// forEach(cb: (symbol: Symbol) => void): void; +// forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; +// } /**@internal */ export interface UsageInfo { // Symbols whose declarations are moved from the old file to the new file. - readonly movedSymbols: ReadonlySymbolSet; + readonly movedSymbols: Set; // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) - readonly newFileImportsFromOldFile: ReadonlySymbolSet; + readonly newFileImportsFromOldFile: Set; // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. - readonly oldFileImportsFromNewFile: ReadonlySymbolSet; + readonly oldFileImportsFromNewFile: Set; - readonly oldImportsNeededByNewFile: ReadonlySymbolSet; + readonly oldImportsNeededByNewFile: Map; // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. - readonly unusedImportsFromOldFile: ReadonlySymbolSet; + readonly unusedImportsFromOldFile: Set; readonly moduleSyntax: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; } type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' @@ -621,16 +621,16 @@ function isPureImport(node: Node): boolean { /** @internal */ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { - const movedSymbols = new SymbolSet(); - const oldImportsNeededByNewFile = new SymbolSet(); - const newFileImportsFromOldFile = new SymbolSet(); + const movedSymbols = new Set(); + const oldImportsNeededByNewFile = new Map(); + const newFileImportsFromOldFile = new Set(); const moduleSyntax = (oldFile.commonJsModuleIndicator) ? ModuleKind.CommonJS : (oldFile.externalModuleIndicator) ? ModuleKind.ESNext : undefined; 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); + oldImportsNeededByNewFile.set(jsxNamespaceSymbol, false); } for (const statement of toMove) { @@ -639,11 +639,12 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], }); } for (const statement of toMove) { - forEachReference(statement, checker, symbol => { + forEachReference(statement, checker, (symbol, isValidTypeOnlyUseSite) => { if (!symbol.declarations) return; for (const decl of symbol.declarations) { if (isInImport(decl)) { - oldImportsNeededByNewFile.add(symbol); + const prevIsTypeOnly = oldImportsNeededByNewFile.get(symbol); + oldImportsNeededByNewFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); } else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { newFileImportsFromOldFile.add(symbol); @@ -651,10 +652,9 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], } }); } + const unusedImportsFromOldFile = new Set(oldImportsNeededByNewFile.keys()); - const unusedImportsFromOldFile = oldImportsNeededByNewFile.clone(); - - const oldFileImportsFromNewFile = new SymbolSet(); + const oldFileImportsFromNewFile = new Set(); for (const statement of oldFile.statements) { if (contains(toMove, statement)) continue; @@ -698,15 +698,15 @@ function makeUniqueFilename(proposedFilename: string, extension: string, inDirec } } -function inferNewFilename(importsFromNewFile: ReadonlySymbolSet, movedSymbols: ReadonlySymbolSet): string { - return importsFromNewFile.forEachEntry(symbolNameNoDefault) || movedSymbols.forEachEntry(symbolNameNoDefault) || "newFile"; +function inferNewFilename(importsFromNewFile: Set, movedSymbols: Set): string { + return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; } -function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol) => void) { +function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { node.forEachChild(function cb(node) { if (isIdentifier(node) && !isDeclarationName(node)) { const sym = checker.getSymbolAtLocation(node); - if (sym) onReference(sym); + if (sym) onReference(sym, isValidTypeOnlyAliasUseSite(node)); } else { node.forEachChild(cb); @@ -793,38 +793,3 @@ export function isNonVariableTopLevelDeclaration(node: Node): node is NonVariabl return false; } } - -/**@internal */ -export interface ReadonlySymbolSet { - size(): number; - has(symbol: Symbol): boolean; - forEach(cb: (symbol: Symbol) => void): void; - forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -} - -class SymbolSet implements ReadonlySymbolSet { - private map = new Map(); - 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(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; - } -} diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 1212a7b7d7d01..6ac7f06a374db 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -22,13 +22,14 @@ import { RefactorEditInfo, skipAlias, SourceFile, + Symbol, SyntaxKind, takeWhile, textChanges, TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; @@ -86,7 +87,7 @@ function getNewStatementsAndRemoveFromOldFile( ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); - if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size() === 0 && !newFileExists) { + if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size === 0 && !newFileExists) { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } @@ -135,8 +136,8 @@ function getNewStatementsAndRemoveFromOldFile( function getNewFileImportsAndAddExportInOldFile( oldFile: SourceFile, newFile: string, - importsToCopy: ReadonlySymbolSet, - newFileImportsFromOldFile: ReadonlySymbolSet, + importsToCopy: Map, + newFileImportsFromOldFile: Set, changes: textChanges.ChangeTracker, checker: TypeChecker, program: Program, @@ -147,8 +148,8 @@ function getNewFileImportsAndAddExportInOldFile( ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; if (importAdder) { - importsToCopy.forEach(symbol => { - importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); + importsToCopy.forEach((isValidTypeOnlyUseSite, symbol) => { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker), isValidTypeOnlyUseSite); }); } else { diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index cfd1ba19ba1ef..2582a94e1b549 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -20,6 +20,7 @@ import { RefactorContext, RefactorEditInfo, SourceFile, + Symbol, SyntaxKind, takeWhile, textChanges, @@ -27,7 +28,7 @@ import { UserPreferences, } from "../_namespaces/ts"; import { registerRefactor } from "../_namespaces/ts.refactor"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createNewFilename, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, ReadonlySymbolSet, SupportedImportStatement, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { addExports, addExportToChanges, addNewFileToTsconfig, createNewFilename, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, SupportedImportStatement, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; const refactorName = "Move to a new file"; const description = getLocaleSpecificMessage(Diagnostics.Move_to_a_new_file); @@ -76,7 +77,7 @@ function getNewStatementsAndRemoveFromOldFile( ) { 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.oldImportsNeededByNewFile.size === 0) { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } @@ -112,8 +113,8 @@ function getNewStatementsAndRemoveFromOldFile( function getNewFileImportsAndAddExportInOldFile( oldFile: SourceFile, - importsToCopy: ReadonlySymbolSet, - newFileImportsFromOldFile: ReadonlySymbolSet, + importsToCopy: Map, + newFileImportsFromOldFile: Set, changes: textChanges.ChangeTracker, checker: TypeChecker, program: Program, diff --git a/tests/cases/fourslash/moveToFile_ctsTomts.ts b/tests/cases/fourslash/moveToFile_ctsTomts.ts index d2c057270dafd..250ddcbe3c9b4 100644 --- a/tests/cases/fourslash/moveToFile_ctsTomts.ts +++ b/tests/cases/fourslash/moveToFile_ctsTomts.ts @@ -20,7 +20,7 @@ verify.moveToFile({ newFileContents: { "/src/dir1/a.cts": -`import { y } from '../dir2/bar.mjs'; +`import { y } from '../dir2/bar.mts'; export const p = 0; y;`, diff --git a/tests/cases/fourslash/moveToFile_typeImport1.ts b/tests/cases/fourslash/moveToFile_typeImport.ts similarity index 78% rename from tests/cases/fourslash/moveToFile_typeImport1.ts rename to tests/cases/fourslash/moveToFile_typeImport.ts index 5abb0023a5e7e..672015eeb7c77 100644 --- a/tests/cases/fourslash/moveToFile_typeImport1.ts +++ b/tests/cases/fourslash/moveToFile_typeImport.ts @@ -1,6 +1,10 @@ /// +// @verbatimModuleSyntax: true +//@module: esnext +//moduleResolution: bundler + // @Filename: /bar.ts ////import {} from "./somefile"; @@ -22,7 +26,7 @@ verify.moveToFile({ `import { type A } from "./other"; `, "/bar.ts": -`import { B } from "./other"; +`import type { B } from "./other"; import {} from "./somefile"; function f(a: B) { } `, diff --git a/tests/cases/fourslash/moveToFile_typeImport2.ts b/tests/cases/fourslash/moveToFile_typeImport2.ts deleted file mode 100644 index 1850a3786730e..0000000000000 --- a/tests/cases/fourslash/moveToFile_typeImport2.ts +++ /dev/null @@ -1,27 +0,0 @@ -/// - -// @Filename: /bar.ts -//// import type {} from './a'; -//// - -// @Filename: /a.ts -////export const p = 0; -////export const b = 1; -////[|const y = p + b;|] - - - -verify.moveToFile({ - newFileContents: { - "/a.ts": -`export const p = 0; -export const b = 1; -`, - - "/bar.ts": -`import { b, p } from './a'; -const y = p + b; -`, - }, - newFile: "/bar.ts", -}); From 486293161ec9a6fa231df60a44ba1e6895c48c0b Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 10 Apr 2023 13:04:25 -0700 Subject: [PATCH 30/42] Not needed --- src/services/moveToFileAndNewFile.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts index d84c9440be5f0..73e3784ed8674 100644 --- a/src/services/moveToFileAndNewFile.ts +++ b/src/services/moveToFileAndNewFile.ts @@ -1,7 +1,7 @@ import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, flatMap, GetCanonicalFileName, getRangesWhere, last,length,mapDefined,some,tryCast } from "../compiler/core"; import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; -import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, ModuleKind, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; +import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachKey,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isValidTypeOnlyAliasUseSite, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; /**@internal */ @@ -511,7 +511,6 @@ export interface UsageInfo { readonly oldImportsNeededByNewFile: Map; // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. readonly unusedImportsFromOldFile: Set; - readonly moduleSyntax: ModuleKind.CommonJS | ModuleKind.ESNext | undefined; } type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' /**@internal */ @@ -624,7 +623,6 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], const movedSymbols = new Set(); const oldImportsNeededByNewFile = new Map(); const newFileImportsFromOldFile = new Set(); - const moduleSyntax = (oldFile.commonJsModuleIndicator) ? ModuleKind.CommonJS : (oldFile.externalModuleIndicator) ? ModuleKind.ESNext : undefined; const containsJsx = find(toMove, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx); @@ -669,7 +667,7 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], }); } - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile, moduleSyntax }; + return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; function getJsxNamespaceSymbol(containsJsx: Node | undefined) { if (containsJsx === undefined) { From f0e870da50353a3c001cf92a6a6e29363ce12ad5 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 12 Apr 2023 09:23:52 -0700 Subject: [PATCH 31/42] Adressing pr comments --- src/harness/client.ts | 6 +- src/harness/harnessLanguageService.ts | 2 +- src/server/protocol.ts | 12 +- src/server/session.ts | 46 +- src/services/codefixes/importFixes.ts | 2 +- src/services/moveToFileAndNewFile.ts | 793 ---------------- src/services/refactors/moveToFile.ts | 898 +++++++++++++++++- src/services/refactors/moveToNewFile.ts | 25 +- src/services/services.ts | 10 +- src/services/types.ts | 6 +- .../unittests/tsserver/refactors.ts | 2 +- .../moveToFile_differentDirectories2.ts | 33 + .../fourslash/moveToFile_impossibleImport.ts | 39 + .../fourslash/moveToFile_unresolvedImport.ts | 21 + 14 files changed, 1047 insertions(+), 848 deletions(-) delete mode 100644 src/services/moveToFileAndNewFile.ts create mode 100644 tests/cases/fourslash/moveToFile_differentDirectories2.ts create mode 100644 tests/cases/fourslash/moveToFile_impossibleImport.ts create mode 100644 tests/cases/fourslash/moveToFile_unresolvedImport.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index c46239b58d14f..5182a8dd4d08b 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -793,12 +793,12 @@ export class SessionClient implements LanguageService { return response.body!; // TODO: GH#18217 } - getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFilename: string | undefined; files: string[] | undefined; } { + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string | undefined; files: string[] | undefined; } { const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); const request = this.processRequest(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args); - const response = this.processResponse(request); - return { newFilename: response.body?.newFileName, files:response.body?.files }!; // TODO: GH#18217 + const response = this.processResponse(request); + return { newFileName: response.body?.newFileName, files:response.body?.files }!; } getEditsForRefactor( diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index e08ce015716d2..5d48bc6bea496 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -619,7 +619,7 @@ class LanguageServiceShimProxy implements ts.LanguageService { getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } - getMoveToRefactoringFileSuggestions(): { newFilename: string | undefined, files: string[] | undefined } { + getMoveToRefactoringFileSuggestions(): { newFileName: string | undefined, files: string[] | undefined } { throw new Error("Not supported on the shim."); } organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 5a0ff29f369b2..560a574925c58 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -614,11 +614,11 @@ export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRange * 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 GetMoveToRefactoringFileSuggestionsResponse extends Response { - body?: { - newFileName: string; - files: string[]; - }; +export interface GetMoveToRefactoringFileSuggestions extends Response { + body?: { + newFileName: string; + files: string[]; + }; } /** @@ -711,7 +711,7 @@ export type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeReques /* The 'name' property from the refactoring action */ action: string; /* Target file path that the user selected (may also be new file) */ - filepath: string; + targetFile: string; }; export interface RefactorEditInfo { diff --git a/src/server/session.ts b/src/server/session.ts index 7bb255a816c21..5a90a98dd5222 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2704,35 +2704,33 @@ export class Session 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 | undefined, files: string[] | undefined }{ - const { file } = this.getFileAndProject(args); + const { file, project } = this.getFileAndProject(args); const allFiles: string[] = []; - let filename: string | undefined; - this.projectService.forEachEnabledProject(project => { - updateProjectIfDirty(project); - if (project.containsFile(file)) { - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - if (scriptInfo) { - const { newFilename, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); - if (files) { - for (const file of files) { - if (!allFiles.includes(file)) { - allFiles.push(file); - } - } + let fileName: string | undefined; + updateProjectIfDirty(project); + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + if (scriptInfo) { + const { newFileName, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); + if (files) { + for (const file of files) { + if (!allFiles.includes(file)) { + allFiles.push(file); } - filename = newFilename; } } - }); - return { newFilename: filename, files: allFiles }; + fileName = newFileName; + } + return { newFilename: fileName, files: allFiles }; } private getEditsForMoveToFileForRefactor(args: protocol.GetEditsForMoveToFileRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { @@ -2740,7 +2738,7 @@ export class Session implements EventSender { const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; const result = project.getLanguageService().getEditsForMoveToFileRefactor( file, - args.filepath, + args.targetFile, this.getFormatOptions(file), this.extractPositionOrRange(args, scriptInfo), args.refactor, @@ -2763,9 +2761,7 @@ export class Session implements EventSender { } return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) }; } - else { - return result; - } + return result; } private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ca7e33e060ba0..6e7f9916748b7 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -594,7 +594,7 @@ function getImportFixes( sourceFile: SourceFile, host: LanguageServiceHost, preferences: UserPreferences, - importMap = sourceFile.kind && createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), + importMap = createExistingImportMap(program.getTypeChecker(), sourceFile, program.getCompilerOptions()), fromCacheOnly?: boolean, ): { computedWithoutCacheCount: number, fixes: readonly ImportFixWithModuleSpecifier[] } { const checker = program.getTypeChecker(); diff --git a/src/services/moveToFileAndNewFile.ts b/src/services/moveToFileAndNewFile.ts deleted file mode 100644 index 73e3784ed8674..0000000000000 --- a/src/services/moveToFileAndNewFile.ts +++ /dev/null @@ -1,793 +0,0 @@ -import { cast, concatenate, contains, emptyArray, find, findIndex, firstDefined, flatMap, GetCanonicalFileName, getRangesWhere, last,length,mapDefined,some,tryCast } from "../compiler/core"; -import { getModuleSpecifier } from "../compiler/moduleSpecifiers"; -import { combinePaths, getDirectoryPath, getRelativePathFromFile,normalizePath, resolvePath } from "../compiler/path"; -import { AnyImportOrRequireStatement, AssignmentDeclarationKind, BinaryExpression, BindingElement, BindingName, CallExpression, ClassDeclaration, Declaration, DeclarationStatement, EnumDeclaration, Expression, ExpressionStatement, ExternalModuleReference, FunctionDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, InternalSymbolName, ModifierFlags, ModifierLike, ModuleDeclaration, NamedImportBindings, Node, NodeFlags, Program, PropertyAccessExpression, PropertyAssignment, RequireOrImportCall, RequireVariableStatement, ScriptTarget, SourceFile, Statement, StringLiteralLike, SymbolFlags, SyntaxKind, TransformFlags, TypeAliasDeclaration, TypeChecker, TypeNode, VariableDeclaration, VariableDeclarationList, VariableStatement } from "../compiler/types"; -import { canHaveDecorators, canHaveModifiers, canHaveSymbol, codefix, createModuleSpecifierResolutionHost, createTextRangeFromSpan, Debug, escapeLeadingUnderscores, extensionFromPath, factory,FindAllReferences,forEachKey,getAssignmentDeclarationKind,getDecorators,getModifiers,getPropertySymbolFromBindingElement,getRefactorContextSpan,getSynthesizedDeepClone,getUniqueName,hasSyntacticModifier,isArrayLiteralExpression, isBinaryExpression, isBindingElement, isDeclarationName, isExpressionStatement, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamedDeclaration, isObjectLiteralExpression, isOmittedExpression, isPrologueDirective, isPropertyAccessExpression, isPropertyAssignment, isRequireCall, isSourceFile, isStringLiteral, isStringLiteralLike, isValidTypeOnlyAliasUseSite, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, LanguageServiceHost, makeImportIfNecessary, makeStringLiteral, ObjectBindingElementWithoutPropertyName, QuotePreference, rangeContainsRange, RefactorContext, skipAlias, Symbol,symbolNameNoDefault, textChanges } from "./_namespaces/ts"; - -/**@internal */ -export 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); - } -} - -/**@internal */ -export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { - for (const { first, afterLast } of moved) { - changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); - } -} - -/**@internal */ -export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: Set, 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)!))); - } -} - -/**@internal */ -export function updateImportsInOtherFiles( - changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, 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); - }); - } - } -} - -/**@internal */ -export 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}`); - } -} - -/**@internal */ -export function updateNamespaceLikeImport( - changes: textChanges.ChangeTracker, - sourceFile: SourceFile, - checker: TypeChecker, - movedSymbols: Set, - 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 createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { - return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); -} - -/**@internal */ -export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { - return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression - : i.initializer.arguments[0]); -} - -/**@internal */ -export 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 }; - -/**@internal */ -export type SupportedImportStatement = - | ImportDeclaration - | ImportEqualsDeclaration - | VariableStatement; - -/**@internal */ -export function createOldFileImportsFromNewFile( - sourceFile: SourceFile, - newFileNeedExport: Set, - 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); -} - -/**@internal */ -export 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)); -} - -/**@internal */ -export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Set, 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(getSynthesizedDeepClone(statement), useEs6Exports); - if (exports) return exports; - } - return getSynthesizedDeepClone(statement); - }); -} - -/**@internal */ -export 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))); -} - -/**@internal */ -export 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; - } -} - -type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; - -function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { - Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); - return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); -} - -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(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)]; -} - -/** 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))); -} - -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}`); - } -} - -/**@internal */ -export 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), getSynthesizedDeepClone(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; - } - } -} - -/**@internal */ -export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { - return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); -} - -/**@internal */ -export 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; - } -} - -/**@internal */ -export 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)); - } -} - -//creating a new filename - -/**@internal */ -export interface ToMove { - readonly all: readonly Statement[]; - readonly ranges: readonly StatementRange[]; -} - -/**@internal */ -export interface StatementRange { - readonly first: Statement; - readonly afterLast: Statement | undefined; -} - -// /**@internal */ -// export interface Set { -// size(): number; -// has(symbol: Symbol): boolean; -// forEach(cb: (symbol: Symbol) => void): void; -// forEachEntry(cb: (symbol: Symbol) => T | undefined): T | undefined; -// } - -/**@internal */ -export interface UsageInfo { - // Symbols whose declarations are moved from the old file to the new file. - readonly movedSymbols: Set; - - // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) - readonly newFileImportsFromOldFile: Set; - // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. - readonly oldFileImportsFromNewFile: Set; - - readonly oldImportsNeededByNewFile: Map; - // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. - readonly unusedImportsFromOldFile: Set; -} -type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' -/**@internal */ -export type NonVariableTopLevelDeclaration = - | FunctionDeclaration - | ClassDeclaration - | EnumDeclaration - | TypeAliasDeclaration - | InterfaceDeclaration - | ModuleDeclaration - | TopLevelExpressionStatement - | ImportEqualsDeclaration; - -/**@internal */ -export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } - /**@internal */ -export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; - -/** @internal */ -export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { - const checker = program.getTypeChecker(); - const toMove = getStatementsToMove(context); - let usage; - if (toMove) { - 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; - return newFilename; - } - return ""; -} - -/**@internal */ -export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } - -/**@internal */ -export 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], - }; -} - -/**@internal */ -export 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; - } -} - -/** @internal */ -export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { - const movedSymbols = new Set(); - const oldImportsNeededByNewFile = new Map(); - const newFileImportsFromOldFile = new Set(); - - 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.set(jsxNamespaceSymbol, false); - } - - 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, isValidTypeOnlyUseSite) => { - if (!symbol.declarations) return; - for (const decl of symbol.declarations) { - if (isInImport(decl)) { - const prevIsTypeOnly = oldImportsNeededByNewFile.get(symbol); - oldImportsNeededByNewFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); - } - else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { - newFileImportsFromOldFile.add(symbol); - } - } - }); - } - const unusedImportsFromOldFile = new Set(oldImportsNeededByNewFile.keys()); - - const oldFileImportsFromNewFile = new Set(); - 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; - } -} - -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: Set, movedSymbols: Set): string { - return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; -} - -function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { - node.forEachChild(function cb(node) { - if (isIdentifier(node) && !isDeclarationName(node)) { - const sym = checker.getSymbolAtLocation(node); - if (sym) onReference(sym, isValidTypeOnlyAliasUseSite(node)); - } - else { - node.forEachChild(cb); - } - }); -} - -/**@internal */ -export function forEachTopLevelDeclaration(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; - } - } -} -/** @internal */ -export 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); -} - -/**@internal */ -export 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 forEachTopLevelDeclarationInBindingName(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}`); - } -} - -/**@internal */ -export 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; - } -} diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 6ac7f06a374db..acffcea46fdf4 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -1,35 +1,138 @@ +import { getModuleSpecifier } from "../../compiler/moduleSpecifiers"; import { AnyImportOrRequireStatement, append, ApplicableRefactorInfo, + AssignmentDeclarationKind, + BinaryExpression, + BindingElement, + BindingName, + CallExpression, + canHaveDecorators, + canHaveModifiers, + canHaveSymbol, + cast, + ClassDeclaration, codefix, + combinePaths, + concatenate, + contains, + createModuleSpecifierResolutionHost, + createTextRangeFromSpan, Debug, + Declaration, + DeclarationStatement, Diagnostics, emptyArray, + EnumDeclaration, + escapeLeadingUnderscores, + Expression, + ExpressionStatement, + Extension, + extensionFromPath, + ExternalModuleReference, + factory, + find, + FindAllReferences, + findIndex, + firstDefined, + flatMap, + forEachKey, + FunctionDeclaration, + getAssignmentDeclarationKind, + GetCanonicalFileName, + getDecorators, + getDirectoryPath, + getImpliedNodeFormatForFile, getLocaleSpecificMessage, + getModeForUsageLocation, + getModifiers, + getPropertySymbolFromBindingElement, getQuotePreference, + getRangesWhere, + getRefactorContextSpan, + getRelativePathFromFile, + getSynthesizedDeepClone, + 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, + isValidTypeOnlyAliasUseSite, + isVariableDeclaration, + isVariableDeclarationList, + isVariableStatement, LanguageServiceHost, + last, + length, + makeImportIfNecessary, + makeStringLiteral, + mapDefined, ModifierFlags, + ModifierLike, + ModuleDeclaration, + ModuleKind, + 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 { addExports, addExportToChanges, addNewFileToTsconfig, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire, moduleSpecifierFromImport, nameOfTopLevelDeclaration, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; import { registerRefactor } from "../refactorProvider"; const refactorNameForMoveToFile = "Move to file"; @@ -57,11 +160,9 @@ registerRefactor(refactorNameForMoveToFile, { getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, newFile): RefactorEditInfo | undefined { Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - if (newFile) { - const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); - return { edits, renameFilename: undefined, renameLocation: undefined }; - } - return undefined; + Debug.assert(newFile, "Target file does not exist"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); + return { edits, renameFilename: undefined, renameLocation: undefined }; } }); @@ -92,7 +193,13 @@ function getNewStatementsAndRemoveFromOldFile( return [...prologueDirectives, ...toMove.all]; } - const useEsModuleSyntax = !!oldFile.externalModuleIndicator; + let useEsModuleSyntax = true; + if (newFileExists) { + const newFilePath = program.getSourceFile(newFile)?.path; + if (extensionFromPath(newFile) === Extension.Js && newFilePath && getImpliedNodeFormatForFile(newFilePath, /*packageJsonInfoCache*/ undefined, host, program.getCompilerOptions()) !== ModuleKind.ESNext && (oldFile.commonJsModuleIndicator || program.getCompilerOptions().verbatimModuleSyntax)) { + useEsModuleSyntax = false; + } + } const quotePreference = getQuotePreference(oldFile, preferences); const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); if (importsFromNewFile) { @@ -149,13 +256,33 @@ function getNewFileImportsAndAddExportInOldFile( const copiedOldImports: AnyImportOrRequireStatement[] = []; if (importAdder) { importsToCopy.forEach((isValidTypeOnlyUseSite, symbol) => { - importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker), isValidTypeOnlyUseSite); + try { + importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker), isValidTypeOnlyUseSite); + } + catch (e) { + if (checker.isUnknownSymbol(symbol)) { + throw e; + } + } }); } else { for (const oldStatement of oldFile.statements) { forEachImportInStatement(oldStatement, i => { - append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + const moduleSpecifier = moduleSpecifierFromImport(i); + const resolved = oldFile.resolvedModules?.get(moduleSpecifier.text, getModeForUsageLocation(oldFile, moduleSpecifier)); + const fileName = resolved?.resolvedModule?.resolvedFileName; + if (fileName) { + const sourceFile = program.getSourceFile(newFile); + if (sourceFile) { + const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), fileName); + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host)); + append(copiedOldImports, filterImport(i, makeStringLiteral(newModuleSpecifier, quotePreference), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + } + } + else { + append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + } }); } } @@ -178,7 +305,7 @@ function getNewFileImportsAndAddExportInOldFile( if (markSeenTop(top)) { addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); } - if (importAdder && symbol.parent !== undefined) { + if (importAdder && checker.isUnknownSymbol(symbol)) { importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker)); } else { @@ -196,3 +323,754 @@ function getNewFileImportsAndAddExportInOldFile( : append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)); } +export 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); + } +} + +export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { + for (const { first, afterLast } of moved) { + changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); + } +} + +export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: Set, 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)!))); + } +} + +export function updateImportsInOtherFiles( + changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, 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); + }); + } + } +} + +export 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}`); + } +} + +export function updateNamespaceLikeImport( + changes: textChanges.ChangeTracker, + sourceFile: SourceFile, + checker: TypeChecker, + movedSymbols: Set, + 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 createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { + return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); +} + +export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { + return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier + : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression + : i.initializer.arguments[0]); +} + +export 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 }; + +export type SupportedImportStatement = + | ImportDeclaration + | ImportEqualsDeclaration + | VariableStatement; + +export function createOldFileImportsFromNewFile( + sourceFile: SourceFile, + newFileNeedExport: Set, + 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); +} + +export 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)); +} + +export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Set, 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(getSynthesizedDeepClone(statement), useEs6Exports); + if (exports) return exports; + } + return getSynthesizedDeepClone(statement); + }); +} + +export 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))); +} + +export 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; + } +} + +type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; + +function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { + Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); + return isNonVariableTopLevelDeclaration(node) || isVariableStatement(node); +} + +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(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: + // Shouldn't try to add 'export' keyword to `exports.x = ...` + return Debug.fail(); + 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)]; +} + +/** 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))); +} + +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: + // Shouldn't try to add 'export' keyword to `exports.x = ...` + return Debug.fail("Can't export an ExpressionStatement"); + default: + return Debug.assertNever(decl, `Unexpected decl kind ${(decl as TopLevelDeclarationStatement).kind}`); + } +} + +export 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), getSynthesizedDeepClone(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; + } + } +} + +export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { + return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); +} + +export 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; + } +} + +export 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)); + } +} + +//creating a new filename +export interface ToMove { + readonly all: readonly Statement[]; + readonly ranges: readonly StatementRange[]; +} + +export interface StatementRange { + readonly first: Statement; + readonly afterLast: Statement | undefined; +} + +export interface UsageInfo { + // Symbols whose declarations are moved from the old file to the new file. + readonly movedSymbols: Set; + + // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) + readonly newFileImportsFromOldFile: Set; + // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. + readonly oldFileImportsFromNewFile: Set; + + readonly oldImportsNeededByNewFile: Map; + // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. + readonly unusedImportsFromOldFile: Set; +} +type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' + +export type NonVariableTopLevelDeclaration = + | FunctionDeclaration + | ClassDeclaration + | EnumDeclaration + | TypeAliasDeclaration + | InterfaceDeclaration + | ModuleDeclaration + | TopLevelExpressionStatement + | ImportEqualsDeclaration; + +export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } +export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; + +export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { + const checker = program.getTypeChecker(); + const toMove = getStatementsToMove(context); + let usage; + if (toMove) { + 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; + return newFilename; + } + return ""; +} + +export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } + +export 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], + }; +} + +export 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; + } +} + +export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { + const movedSymbols = new Set(); + const oldImportsNeededByNewFile = new Map(); + const newFileImportsFromOldFile = new Set(); + + 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.set(jsxNamespaceSymbol, false); + } + + 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, isValidTypeOnlyUseSite) => { + if (!symbol.declarations) return; + for (const decl of symbol.declarations) { + if (isInImport(decl)) { + const prevIsTypeOnly = oldImportsNeededByNewFile.get(symbol); + oldImportsNeededByNewFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); + } + else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { + newFileImportsFromOldFile.add(symbol); + } + } + }); + } + const unusedImportsFromOldFile = new Set(oldImportsNeededByNewFile.keys()); + + const oldFileImportsFromNewFile = new Set(); + 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; + } +} + +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: Set, movedSymbols: Set): string { + return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; +} + +function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { + node.forEachChild(function cb(node) { + if (isIdentifier(node) && !isDeclarationName(node)) { + const sym = checker.getSymbolAtLocation(node); + if (sym) onReference(sym, isValidTypeOnlyAliasUseSite(node)); + } + else { + node.forEachChild(cb); + } + }); +} + +export function forEachTopLevelDeclaration(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; + } + } +} + +export 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); +} + +export 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 forEachTopLevelDeclarationInBindingName(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}`); + } +} + +export 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; + } +} + + diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 2582a94e1b549..67daf4c37716a 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -27,8 +27,29 @@ import { TypeChecker, UserPreferences, } from "../_namespaces/ts"; -import { registerRefactor } from "../_namespaces/ts.refactor"; -import { addExports, addExportToChanges, addNewFileToTsconfig, createNewFilename, createOldFileImportsFromNewFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, forEachImportInStatement, getStatementsToMove, getTopLevelDeclarationStatement, getUsageInfo, isTopLevelDeclaration, makeImportOrRequire,moduleSpecifierFromImport, nameOfTopLevelDeclaration, SupportedImportStatement, ToMove, updateImportsInOtherFiles, UsageInfo } from "../moveToFileAndNewFile"; +import { + addExports, + addExportToChanges, + addNewFileToTsconfig, + createNewFilename, + createOldFileImportsFromNewFile, + 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); diff --git a/src/services/services.ts b/src/services/services.ts index e87ecae04b5ac..9906c6691f300 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -322,9 +322,9 @@ 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"; -import { createNewFilename } from "./moveToFileAndNewFile"; /** The version of the language service API */ export const servicesVersion = "0.8"; @@ -2978,7 +2978,7 @@ export function createLanguageService( return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); } - function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFilename: string | undefined, files: string[] | undefined } { + function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFileName: string | undefined, files: string[] | undefined } { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const program = getProgram(); @@ -2997,11 +2997,11 @@ export function createLanguageService( } } //creating new filename - let newFilename; + let newFileName; if (program) { - newFilename = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); + newFileName = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); } - return { newFilename, files }; + return { newFileName, files }; } function getEditsForRefactor( diff --git a/src/services/types.ts b/src/services/types.ts index ba314641382d8..5417b102eedbc 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -631,7 +631,7 @@ export interface LanguageService { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; - getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFilename: string | undefined, files: string[] | undefined }; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFileName: string | undefined, files: string[] | undefined }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; @@ -671,6 +671,10 @@ export interface LinkedEditingInfo { export interface CombinedCodeFixScope { type: "file"; fileName: string; } +export interface MoveToFileArgs { + newFileName: string; +} + export const enum OrganizeImportsMode { All = "All", SortAndCombine = "SortAndCombine", diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index 49d6f28805915..32dac0f5b633e 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -115,7 +115,7 @@ const a = 1;`}; endOffset: aTs.content.length, refactor: "Move to file", action: "Move to file", - filepath: "/Foo/b.ts", + targetFile: "/Foo/b.ts", } }); baselineTsserverLogs("refactors", "handles moving statement to an existing file", session); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories2.ts b/tests/cases/fourslash/moveToFile_differentDirectories2.ts new file mode 100644 index 0000000000000..c317761a99f3a --- /dev/null +++ b/tests/cases/fourslash/moveToFile_differentDirectories2.ts @@ -0,0 +1,33 @@ +/// + +//@moduleResolution: bundler +//@module: esnext + +// @Filename: /src/dir1/a.ts +////import { b } from './other'; +////const a = 10; +////[|const y = b + a;|] +////y; + +// @Filename: /src/dir1/other.ts +////export const b = 1; + +//@Filename: /src/dir2/bar.ts +//// + +verify.moveToFile({ + newFileContents: { + "/src/dir1/a.ts": +`import { y } from '../dir2/bar'; +export const a = 10; +y;`, + + "/src/dir2/bar.ts": +`import { b } from '../dir1/other'; +import { a } from '../dir1/a'; + +export const y = b + a; +`, + }, + newFile: "/src/dir2/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile_impossibleImport.ts b/tests/cases/fourslash/moveToFile_impossibleImport.ts new file mode 100644 index 0000000000000..534b4594b70c4 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_impossibleImport.ts @@ -0,0 +1,39 @@ +/// + +// @module: nodenext + +// @Filename: /bar.cts +////import './blah.ts'; +////const a = 2; + +// @Filename: /node_modules/esm-only/package.json +//// { +//// "name": "esm-only", +//// "version": "1.0.0", +//// "type": "module", +//// "exports": { +//// ".": { +//// "import": "./index.js" +//// } +//// } +//// } + +// @Filename: /node_modules/esm-only/index.d.ts +//// export declare const esm: any; + +// @Filename: /main.mts +//// import { esm } from "esm-only"; +//// [|esm.ohno;|] + +verify.moveToFile({ + newFileContents: { + "/main.mts":``, + + "/bar.cts": +`import './blah.ts'; +const a = 2; +esm.ohno; +`, + }, + newFile: "/bar.cts", +}); \ No newline at end of file diff --git a/tests/cases/fourslash/moveToFile_unresolvedImport.ts b/tests/cases/fourslash/moveToFile_unresolvedImport.ts new file mode 100644 index 0000000000000..6028e62be6ca3 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_unresolvedImport.ts @@ -0,0 +1,21 @@ +/// + +// @Filename: /bar.ts +////const a = 1; + +// @Filename: /a.ts +////import { a } from 'doesnt-exist'; +////[|a();|] + +verify.moveToFile({ + newFileContents: { + "/a.ts": +``, + + "/bar.ts": +`const a = 1; +a(); +`, + }, + newFile: "/bar.ts", +}); \ No newline at end of file From b0104446575305056453b0c114fbe1a8f8f87a42 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Wed, 12 Apr 2023 15:51:24 -0700 Subject: [PATCH 32/42] adding new tests --- src/harness/client.ts | 2 +- src/services/refactors/moveToFile.ts | 12 +++++---- .../fourslash/moveToFile_impossibleImport.ts | 4 +-- .../fourslash/moveToFile_noImportExport.ts | 27 +++++++++++++++++++ .../fourslash/moveToFile_unresolvedImport.ts | 4 ++- 5 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 tests/cases/fourslash/moveToFile_noImportExport.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 5182a8dd4d08b..d21d22e9507bd 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -824,7 +824,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 { diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index acffcea46fdf4..5d376e9e322a9 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -188,7 +188,7 @@ function getNewStatementsAndRemoveFromOldFile( ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); - if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size === 0 && !newFileExists) { + if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size === 0 && usage.newFileImportsFromOldFile.size === 0 && !newFileExists) { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } @@ -259,9 +259,11 @@ function getNewFileImportsAndAddExportInOldFile( try { importAdder.addImportFromExportedSymbol(skipAlias(symbol, checker), isValidTypeOnlyUseSite); } - catch (e) { - if (checker.isUnknownSymbol(symbol)) { - throw e; + catch { + for (const oldStatement of oldFile.statements) { + forEachImportInStatement(oldStatement, i => { + append(copiedOldImports, filterImport(i, factory.createStringLiteral(moduleSpecifierFromImport(i).text), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + }); } } }); @@ -548,7 +550,7 @@ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], export 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 !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol && sourceFile.symbol.exports?.has(name.escapedText)); } return !!sourceFile.symbol && !!sourceFile.symbol.exports && getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); diff --git a/tests/cases/fourslash/moveToFile_impossibleImport.ts b/tests/cases/fourslash/moveToFile_impossibleImport.ts index 534b4594b70c4..c3ddfaf1aeef0 100644 --- a/tests/cases/fourslash/moveToFile_impossibleImport.ts +++ b/tests/cases/fourslash/moveToFile_impossibleImport.ts @@ -3,7 +3,6 @@ // @module: nodenext // @Filename: /bar.cts -////import './blah.ts'; ////const a = 2; // @Filename: /node_modules/esm-only/package.json @@ -30,8 +29,7 @@ verify.moveToFile({ "/main.mts":``, "/bar.cts": -`import './blah.ts'; -const a = 2; +`const a = 2; esm.ohno; `, }, diff --git a/tests/cases/fourslash/moveToFile_noImportExport.ts b/tests/cases/fourslash/moveToFile_noImportExport.ts new file mode 100644 index 0000000000000..548d95d607639 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_noImportExport.ts @@ -0,0 +1,27 @@ +/// + +//@Filename: /bar.ts +////const q = "test"; + +// @Filename: /a.ts +////const z = 1; +////[|const y = z + 2;|] +////const t = y + 1; + +verify.moveToFile({ + newFileContents: { + "/a.ts": +`import { y } from "./bar"; + +export const z = 1; +const t = y + 1;`, + + "/bar.ts": +`import { z } from "./a"; + +const q = "test"; +export const y = z + 2; +`, + }, + newFile: "/bar.ts", +}); diff --git a/tests/cases/fourslash/moveToFile_unresolvedImport.ts b/tests/cases/fourslash/moveToFile_unresolvedImport.ts index 6028e62be6ca3..24b842cea42ea 100644 --- a/tests/cases/fourslash/moveToFile_unresolvedImport.ts +++ b/tests/cases/fourslash/moveToFile_unresolvedImport.ts @@ -13,7 +13,9 @@ verify.moveToFile({ ``, "/bar.ts": -`const a = 1; +`import { a } from "doesnt-exist"; + +const a = 1; a(); `, }, From 5ecf83daa1842dde3955c99b8fd56b0e9eedfac8 Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Thu, 13 Apr 2023 17:45:40 +0000 Subject: [PATCH 33/42] Fixing errors, accepting baselines --- src/services/refactors/moveToFile.ts | 44 +++++++++++++++++-- src/services/types.ts | 4 -- .../reference/api/tsserverlibrary.d.ts | 6 +-- tests/baselines/reference/api/typescript.d.ts | 2 +- ...es-moving-statement-to-an-existing-file.js | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 5d376e9e322a9..25bf5be29ba10 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -325,6 +325,7 @@ function getNewFileImportsAndAddExportInOldFile( : append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)); } +/** @internal */ export function addNewFileToTsconfig(program: Program, changes: textChanges.ChangeTracker, oldFileName: string, newFileNameWithExtension: string, getCanonicalFileName: GetCanonicalFileName): void { const cfg = program.getCompilerOptions().configFile; if (!cfg) return; @@ -340,12 +341,14 @@ export function addNewFileToTsconfig(program: Program, changes: textChanges.Chan } } +/** @internal */ export function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { for (const { first, afterLast } of moved) { changes.deleteNodeRangeExcludingEnd(sourceFile, first, afterLast); } } +/** @internal */ export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[], changes: textChanges.ChangeTracker, toDelete: Set, checker: TypeChecker) { for (const statement of oldFile.statements) { if (contains(toMove, statement)) continue; @@ -353,6 +356,7 @@ export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Sta } } +/** @internal */ export function updateImportsInOtherFiles( changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, newFilename: string, quotePreference: QuotePreference ): void { @@ -383,6 +387,7 @@ export function updateImportsInOtherFiles( } } +/** @internal */ export function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { switch (node.kind) { case SyntaxKind.ImportDeclaration: @@ -397,6 +402,7 @@ export function getNamespaceLikeImport(node: SupportedImport): Identifier | unde } } +/** @internal */ export function updateNamespaceLikeImport( changes: textChanges.ChangeTracker, sourceFile: SourceFile, @@ -450,12 +456,14 @@ function createRequireCall(moduleSpecifier: StringLiteralLike): CallExpression { return factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [moduleSpecifier]); } +/** @internal */ export function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression : i.initializer.arguments[0]); } +/** @internal */ export function forEachImportInStatement(statement: Statement, cb: (importNode: SupportedImport) => void): void { if (isImportDeclaration(statement)) { if (isStringLiteral(statement.moduleSpecifier)) cb(statement as SupportedImport); @@ -474,16 +482,19 @@ export function forEachImportInStatement(statement: Statement, cb: (importNode: } } -type SupportedImport = +/** @internal */ +export type SupportedImport = | ImportDeclaration & { moduleSpecifier: StringLiteralLike } | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike } } | VariableDeclaration & { initializer: RequireOrImportCall }; +/** @internal */ export type SupportedImportStatement = | ImportDeclaration | ImportEqualsDeclaration | VariableStatement; +/** @internal */ export function createOldFileImportsFromNewFile( sourceFile: SourceFile, newFileNeedExport: Set, @@ -506,6 +517,7 @@ export function createOldFileImportsFromNewFile( return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); } +/** @internal */ export function makeImportOrRequire( sourceFile: SourceFile, defaultImport: Identifier | undefined, @@ -536,6 +548,7 @@ function makeVariableStatement(name: BindingName, type: TypeNode | undefined, in return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(name, /*exclamationToken*/ undefined, type, initializer)], flags)); } +/** @internal */ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Set, useEs6Exports: boolean): readonly Statement[] { return flatMap(toMove, statement => { if (isTopLevelDeclarationStatement(statement) && @@ -548,6 +561,7 @@ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], }); } +/** @internal */ export function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { if (useEs6Exports) { return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol && sourceFile.symbol.exports?.has(name.escapedText)); @@ -556,6 +570,7 @@ export function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStat getNamesToExportInCommonJS(decl).some(name => sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name))); } +/** @internal */ export function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { switch (importDecl.kind) { case SyntaxKind.ImportDeclaration: @@ -636,7 +651,8 @@ function deleteUnusedImportsInVariableDeclaration(sourceFile: SourceFile, varDec } } -type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; +/** @internal */ +export type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); @@ -708,6 +724,7 @@ function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonl } } +/** @internal */ export function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { switch (i.kind) { case SyntaxKind.ImportDeclaration: { @@ -754,10 +771,12 @@ function filterBindingName(name: BindingName, keep: (name: Identifier) => boolea } } +/** @internal */ export function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); } +/** @internal */ export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { switch (d.kind) { case SyntaxKind.VariableDeclaration: @@ -770,6 +789,7 @@ export function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLeve } } +/** @internal */ export function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, name: Identifier, changes: textChanges.ChangeTracker, useEs6Exports: boolean): void { if (isExported(sourceFile, decl, useEs6Exports, name)) return; if (useEs6Exports) { @@ -781,17 +801,19 @@ export function addExportToChanges(sourceFile: SourceFile, decl: TopLevelDeclara } } -//creating a new filename +/** @internal */ export interface ToMove { readonly all: readonly Statement[]; readonly ranges: readonly StatementRange[]; } +/** @internal */ export interface StatementRange { readonly first: Statement; readonly afterLast: Statement | undefined; } +/** @internal */ export interface UsageInfo { // Symbols whose declarations are moved from the old file to the new file. readonly movedSymbols: Set; @@ -805,8 +827,10 @@ export interface UsageInfo { // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. readonly unusedImportsFromOldFile: Set; } -type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' +/** @internal */ +export type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' +/** @internal */ export type NonVariableTopLevelDeclaration = | FunctionDeclaration | ClassDeclaration @@ -817,9 +841,13 @@ export type NonVariableTopLevelDeclaration = | TopLevelExpressionStatement | ImportEqualsDeclaration; +/** @internal */ export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } + +/** @internal */ export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; +/** @internal */ export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { const checker = program.getTypeChecker(); const toMove = getStatementsToMove(context); @@ -845,8 +873,10 @@ export function createNewFilename(oldFile: SourceFile, program: Program, context return ""; } +/** @internal */ export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } +/** @internal */ export function getRangeToMove(context: RefactorContext): RangeToMove | undefined { const { file } = context; const range = createTextRangeFromSpan(getRefactorContextSpan(context)); @@ -872,6 +902,7 @@ export function getRangeToMove(context: RefactorContext): RangeToMove | undefine }; } +/** @internal */ export function getStatementsToMove(context: RefactorContext): ToMove | undefined { const rangeToMove = getRangeToMove(context); if (rangeToMove === undefined) return undefined; @@ -905,6 +936,7 @@ function isPureImport(node: Node): boolean { } } +/** @internal */ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { const movedSymbols = new Set(); const oldImportsNeededByNewFile = new Map(); @@ -998,6 +1030,7 @@ function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Sym }); } +/** @internal */ export function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { switch (statement.kind) { case SyntaxKind.FunctionDeclaration: @@ -1021,6 +1054,7 @@ export function forEachTopLevelDeclaration(statement: Statement, cb: (node: T } } +/** @internal */ export function isInImport(decl: Declaration) { switch (decl.kind) { case SyntaxKind.ImportEqualsDeclaration: @@ -1041,6 +1075,7 @@ function isVariableDeclarationInImport(decl: VariableDeclaration) { !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); } +/** @internal */ export function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); } @@ -1060,6 +1095,7 @@ function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node } } +/** @internal */ export function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { switch (node.kind) { case SyntaxKind.FunctionDeclaration: diff --git a/src/services/types.ts b/src/services/types.ts index 5417b102eedbc..aa4d7cdb6aaf3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -671,10 +671,6 @@ export interface LinkedEditingInfo { export interface CombinedCodeFixScope { type: "file"; fileName: string; } -export interface MoveToFileArgs { - newFileName: string; -} - export const enum OrganizeImportsMode { All = "All", SortAndCombine = "SortAndCombine", diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 32c6a436af5f8..8de50aa0ee467 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -526,7 +526,7 @@ declare namespace ts { * Response is a list of available files. * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring */ - interface GetMoveToRefactoringFileSuggestionsResponse extends Response { + interface GetMoveToRefactoringFileSuggestions extends Response { body?: { newFileName: string; files: string[]; @@ -605,7 +605,7 @@ declare namespace ts { type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeRequestArgs & { refactor: string; action: string; - filepath: string; + targetFile: string; }; interface RefactorEditInfo { edits: FileCodeEdits[]; @@ -10154,7 +10154,7 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { - newFilename: string | undefined; + newFileName: string | undefined; files: string[] | undefined; }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index f1ddf834670cf..b66e472e1372e 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6188,7 +6188,7 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { - newFilename: string | undefined; + newFileName: string | undefined; files: string[] | undefined; }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js index 174ce6cec0d97..367015e6bf171 100644 --- a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -98,7 +98,7 @@ Info seq [hh:mm:ss:mss] request: "endOffset": 12, "refactor": "Move to file", "action": "Move to file", - "filepath": "/Foo/b.ts" + "targetFile": "/Foo/b.ts" }, "seq": 2, "type": "request" From 567a3c5e0b8045e815312d683b4bbbce6ffdbd6a Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:54:52 +0000 Subject: [PATCH 34/42] Removing exports, small changes --- src/harness/client.ts | 2 +- src/server/session.ts | 5 ++++- src/services/refactors/moveToFile.ts | 28 +++++++++++----------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index d21d22e9507bd..fb633e2abaded 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -851,7 +851,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 { edits, diff --git a/src/server/session.ts b/src/server/session.ts index 5a90a98dd5222..f1846332d9c4f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2759,7 +2759,10 @@ export class Session implements EventSender { const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!; mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits); } - return { renameLocation: mappedRenameLocation, renameFilename, edits: this.mapTextChangesToCodeEdits(edits) }; + return { + renameLocation: mappedRenameLocation, + renameFilename, + edits: this.mapTextChangesToCodeEdits(edits) }; } return result; } diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 25bf5be29ba10..19469ea7ec1cf 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -387,8 +387,7 @@ export function updateImportsInOtherFiles( } } -/** @internal */ -export function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { +function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined { switch (node.kind) { case SyntaxKind.ImportDeclaration: return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ? @@ -402,8 +401,7 @@ export function getNamespaceLikeImport(node: SupportedImport): Identifier | unde } } -/** @internal */ -export function updateNamespaceLikeImport( +function updateNamespaceLikeImport( changes: textChanges.ChangeTracker, sourceFile: SourceFile, checker: TypeChecker, @@ -561,8 +559,7 @@ export function addExports(sourceFile: SourceFile, toMove: readonly Statement[], }); } -/** @internal */ -export function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { +function isExported(sourceFile: SourceFile, decl: TopLevelDeclarationStatement, useEs6Exports: boolean, name?: Identifier): boolean { if (useEs6Exports) { return !isExpressionStatement(decl) && hasSyntacticModifier(decl, ModifierFlags.Export) || !!(name && sourceFile.symbol && sourceFile.symbol.exports?.has(name.escapedText)); } @@ -827,6 +824,7 @@ export interface UsageInfo { // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. readonly unusedImportsFromOldFile: Set; } + /** @internal */ export type TopLevelExpressionStatement = ExpressionStatement & { expression: BinaryExpression & { left: PropertyAccessExpression } }; // 'exports.x = ...' @@ -841,7 +839,7 @@ export type NonVariableTopLevelDeclaration = | TopLevelExpressionStatement | ImportEqualsDeclaration; -/** @internal */ + /** @internal */ export interface TopLevelVariableDeclaration extends VariableDeclaration { parent: VariableDeclarationList & { parent: VariableStatement; }; } /** @internal */ @@ -873,11 +871,9 @@ export function createNewFilename(oldFile: SourceFile, program: Program, context return ""; } -/** @internal */ -export interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } +interface RangeToMove { readonly toMove: readonly Statement[]; readonly afterLast: Statement | undefined; } -/** @internal */ -export function getRangeToMove(context: RefactorContext): RangeToMove | undefined { +function getRangeToMove(context: RefactorContext): RangeToMove | undefined { const { file } = context; const range = createTextRangeFromSpan(getRefactorContextSpan(context)); const { statements } = file; @@ -1030,8 +1026,7 @@ function forEachReference(node: Node, checker: TypeChecker, onReference: (s: Sym }); } -/** @internal */ -export function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { +function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevelDeclaration) => T): T | undefined { switch (statement.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: @@ -1054,8 +1049,7 @@ export function forEachTopLevelDeclaration(statement: Statement, cb: (node: T } } -/** @internal */ -export function isInImport(decl: Declaration) { +function isInImport(decl: Declaration) { switch (decl.kind) { case SyntaxKind.ImportEqualsDeclaration: case SyntaxKind.ImportSpecifier: @@ -1070,6 +1064,7 @@ export function isInImport(decl: Declaration) { return false; } } + function isVariableDeclarationInImport(decl: VariableDeclaration) { return isSourceFile(decl.parent.parent.parent) && !!decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true); @@ -1095,8 +1090,7 @@ function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node } } -/** @internal */ -export function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { +function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { switch (node.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: From 8af19ff827fc33b39339719e81ad14dced4c7561 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 17 Apr 2023 11:28:42 -0700 Subject: [PATCH 35/42] Adressing pr comments --- src/services/refactors/moveToFile.ts | 171 +++++++++--------- src/services/refactors/moveToNewFile.ts | 17 +- src/services/services.ts | 4 +- src/services/utilities.ts | 49 +++++ .../fourslash/moveToFile_requireImport2.ts | 41 +++++ 5 files changed, 184 insertions(+), 98 deletions(-) create mode 100644 tests/cases/fourslash/moveToFile_requireImport2.ts diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 25bf5be29ba10..229a11e0ad472 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -28,10 +28,10 @@ import { escapeLeadingUnderscores, Expression, ExpressionStatement, - Extension, extensionFromPath, ExternalModuleReference, factory, + fileShouldUseJavaScriptRequire, find, FindAllReferences, findIndex, @@ -43,7 +43,6 @@ import { GetCanonicalFileName, getDecorators, getDirectoryPath, - getImpliedNodeFormatForFile, getLocaleSpecificMessage, getModeForUsageLocation, getModifiers, @@ -94,7 +93,6 @@ import { ModifierFlags, ModifierLike, ModuleDeclaration, - ModuleKind, NamedImportBindings, Node, NodeFlags, @@ -157,68 +155,62 @@ registerRefactor(refactorNameForMoveToFile, { } return emptyArray; }, - getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, newFile): RefactorEditInfo | undefined { + getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, targetFile): RefactorEditInfo | undefined { Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - Debug.assert(newFile, "Target file does not exist"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, newFile, context.program, statements, t, context.host, context.preferences)); + Debug.assert(targetFile, "Target file does not exist"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, targetFile, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } }); -function doChange(context: RefactorContext, oldFile: SourceFile, newFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { +function doChange(context: RefactorContext, oldFile: SourceFile, targetFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void { const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - //creating a new file - if (!host.fileExists(newFile) || host.fileExists(newFile) && program.getSourceFile(newFile)?.statements.length === 0) { - changes.createNewFile(oldFile, newFile, getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ false)); - addNewFileToTsconfig(program, changes, oldFile.fileName, newFile, hostGetCanonicalFileName(host)); + //For a new file or an existing blank target file + if (!host.fileExists(targetFile) || host.fileExists(targetFile) && program.getSourceFile(targetFile)?.statements.length === 0) { + changes.createNewFile(oldFile, targetFile, getNewStatementsAndRemoveFromOldFile(oldFile, targetFile, usage, changes, toMove, program, host, preferences)); + addNewFileToTsconfig(program, changes, oldFile.fileName, targetFile, hostGetCanonicalFileName(host)); } else { - const sourceFile = program.getSourceFile(newFile); - if (sourceFile) { - const importAdder = codefix.createImportAdder(sourceFile, context.program, context.preferences, context.host); - getNewStatementsAndRemoveFromOldFile(oldFile, newFile, usage, changes, toMove, program, host, newFile, preferences, /*newFileExists*/ true, importAdder); - } + const targetSourceFile = Debug.checkDefined(program.getSourceFile(targetFile)); + const importAdder = codefix.createImportAdder(targetSourceFile, context.program, context.preferences, context.host); + getNewStatementsAndRemoveFromOldFile(oldFile, targetSourceFile, usage, changes, toMove, program, host, preferences, importAdder); } } function getNewStatementsAndRemoveFromOldFile( - oldFile: SourceFile, newFile: string, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, newFilename: string, preferences: UserPreferences, newFileExists: boolean, importAdder?: codefix.ImportAdder + oldFile: SourceFile, targetFile: string | SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, host: LanguageServiceHost, preferences: UserPreferences, importAdder?: codefix.ImportAdder ) { const checker = program.getTypeChecker(); const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); - if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByNewFile.size === 0 && usage.newFileImportsFromOldFile.size === 0 && !newFileExists) { + if (oldFile.externalModuleIndicator === undefined && oldFile.commonJsModuleIndicator === undefined && usage.oldImportsNeededByTargetFile.size === 0 && usage.targetFileImportsFromOldFile.size === 0 && typeof targetFile === "string") { deleteMovedStatements(oldFile, toMove.ranges, changes); return [...prologueDirectives, ...toMove.all]; } - let useEsModuleSyntax = true; - if (newFileExists) { - const newFilePath = program.getSourceFile(newFile)?.path; - if (extensionFromPath(newFile) === Extension.Js && newFilePath && getImpliedNodeFormatForFile(newFilePath, /*packageJsonInfoCache*/ undefined, host, program.getCompilerOptions()) !== ModuleKind.ESNext && (oldFile.commonJsModuleIndicator || program.getCompilerOptions().verbatimModuleSyntax)) { - useEsModuleSyntax = false; - } - } + //If the targetFile is a string, it’s the file name for a new file, if it’s a SourceFile, it’s the existing target file. + const targetFileName = typeof targetFile === "string" ? targetFile : targetFile.fileName; + + const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(targetFileName, program, host, !!oldFile.commonJsModuleIndicator); const quotePreference = getQuotePreference(oldFile, preferences); - const importsFromNewFile = createOldFileImportsFromNewFile(oldFile, usage.oldFileImportsFromNewFile, newFilename, program, host, useEsModuleSyntax, quotePreference); - if (importsFromNewFile) { - insertImports(changes, oldFile, importsFromNewFile, /*blankLineBetween*/ true, preferences); + const importsFromTargetFile = createOldFileImportsFromTargetFile(oldFile, usage.oldFileImportsFromTargetFile, targetFileName, program, host, useEsModuleSyntax, quotePreference); + if (importsFromTargetFile) { + insertImports(changes, oldFile, importsFromTargetFile, /*blankLineBetween*/ true, preferences); } deleteUnusedOldImports(oldFile, toMove.all, changes, usage.unusedImportsFromOldFile, checker); deleteMovedStatements(oldFile, toMove.ranges, changes); - updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, newFilename, quotePreference); - - const imports = getNewFileImportsAndAddExportInOldFile(oldFile, newFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); - const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEsModuleSyntax); - if (newFileExists) { - const sourceFile = program.getSourceFile(newFile); - if (sourceFile && sourceFile.statements.length > 0) { - changes.insertNodesAfter(sourceFile, sourceFile.statements[sourceFile.statements.length - 1], body); + updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, targetFileName, quotePreference); + + const imports = getTargetFileImportsAndAddExportInOldFile(oldFile, targetFileName, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, changes, checker, program, host, useEsModuleSyntax, quotePreference, importAdder); + const body = addExports(oldFile, toMove.all, usage.oldFileImportsFromTargetFile, useEsModuleSyntax); + if (typeof targetFile !== "string") { + if (targetFile.statements.length > 0) { + changes.insertNodesAfter(targetFile, targetFile.statements[targetFile.statements.length - 1], body); } - if (sourceFile && imports.length > 0) { - insertImports(changes, sourceFile, imports, /*blankLineBetween*/ true, preferences); + if (imports.length > 0) { + insertImports(changes, targetFile, imports, /*blankLineBetween*/ true, preferences); } } if (importAdder) { @@ -240,11 +232,11 @@ function getNewStatementsAndRemoveFromOldFile( ]; } -function getNewFileImportsAndAddExportInOldFile( +function getTargetFileImportsAndAddExportInOldFile( oldFile: SourceFile, - newFile: string, + targetFile: string, importsToCopy: Map, - newFileImportsFromOldFile: Set, + targetFileImportsFromOldFile: Set, changes: textChanges.ChangeTracker, checker: TypeChecker, program: Program, @@ -254,6 +246,11 @@ function getNewFileImportsAndAddExportInOldFile( importAdder?: codefix.ImportAdder, ): readonly AnyImportOrRequireStatement[] { const copiedOldImports: AnyImportOrRequireStatement[] = []; + /** + * Recomputing the imports is preferred with importAdder because it manages multiple import additions for a file and writes then to a ChangeTracker, + * but sometimes it fails because of unresolved imports from files, or when a source file is not available for the target file (in this case when creating a new file). + * So in that case, fall back to copying the import verbatim. + */ if (importAdder) { importsToCopy.forEach((isValidTypeOnlyUseSite, symbol) => { try { @@ -269,32 +266,30 @@ function getNewFileImportsAndAddExportInOldFile( }); } else { + const targetSourceFile = program.getSourceFile(targetFile); // Would be undefined for a new file for (const oldStatement of oldFile.statements) { forEachImportInStatement(oldStatement, i => { + // Recomputing module specifier const moduleSpecifier = moduleSpecifierFromImport(i); const resolved = oldFile.resolvedModules?.get(moduleSpecifier.text, getModeForUsageLocation(oldFile, moduleSpecifier)); const fileName = resolved?.resolvedModule?.resolvedFileName; - if (fileName) { - const sourceFile = program.getSourceFile(newFile); - if (sourceFile) { - const pathToNewFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), fileName); - const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToNewFileWithExtension, createModuleSpecifierResolutionHost(program, host)); - append(copiedOldImports, filterImport(i, makeStringLiteral(newModuleSpecifier, quotePreference), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); - } + if (fileName && targetSourceFile) { + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), targetSourceFile, targetSourceFile.path, fileName, createModuleSpecifierResolutionHost(program, host)); + append(copiedOldImports, filterImport(i, makeStringLiteral(newModuleSpecifier, quotePreference), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); } else { - append(copiedOldImports, filterImport(i, moduleSpecifierFromImport(i), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); + append(copiedOldImports, filterImport(i, factory.createStringLiteral(moduleSpecifierFromImport(i).text), name => importsToCopy.has(checker.getSymbolAtLocation(name)!))); } }); } } //Also, import things used from the old file, and insert 'export' modifiers as necessary in the old file. - const newFileSourceFile = program.getSourceFile(newFile); + const targetFileSourceFile = program.getSourceFile(targetFile); let oldFileDefault: Identifier | undefined; const oldFileNamedImports: string[] = []; const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. - newFileImportsFromOldFile.forEach(symbol => { + targetFileImportsFromOldFile.forEach(symbol => { if (!symbol.declarations) { return; } @@ -320,8 +315,8 @@ function getNewFileImportsAndAddExportInOldFile( } } }); - return (newFileSourceFile) - ? append(copiedOldImports, makeImportOrRequire(newFileSourceFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)) + return (targetFileSourceFile) + ? append(copiedOldImports, makeImportOrRequire(targetFileSourceFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)) : append(copiedOldImports, makeImportOrRequire(oldFile, oldFileDefault, oldFileNamedImports, oldFile.fileName, program, host, useEsModuleSyntax, quotePreference)); } @@ -358,7 +353,7 @@ export function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Sta /** @internal */ export function updateImportsInOtherFiles( - changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, newFilename: string, quotePreference: QuotePreference + changes: textChanges.ChangeTracker, program: Program, host: LanguageServiceHost, oldFile: SourceFile, movedSymbols: Set, targetFileName: string, quotePreference: QuotePreference ): void { const checker = program.getTypeChecker(); for (const sourceFile of program.getSourceFiles()) { @@ -370,13 +365,13 @@ export function updateImportsInOtherFiles( const shouldMove = (name: Identifier): boolean => { const symbol = isBindingElement(name.parent) ? getPropertySymbolFromBindingElement(checker, name.parent as ObjectBindingElementWithoutPropertyName) - : skipAlias(checker.getSymbolAtLocation(name)!, checker); // TODO: GH#18217 + : skipAlias(checker.getSymbolAtLocation(name)!, checker); 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 pathToTargetFileWithExtension = resolvePath(getDirectoryPath(oldFile.path), targetFileName); + const newModuleSpecifier = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToTargetFileWithExtension, createModuleSpecifierResolutionHost(program, host)); const newImportDeclaration = filterImport(importNode, makeStringLiteral(newModuleSpecifier, quotePreference), shouldMove); if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); @@ -495,10 +490,10 @@ export type SupportedImportStatement = | VariableStatement; /** @internal */ -export function createOldFileImportsFromNewFile( +export function createOldFileImportsFromTargetFile( sourceFile: SourceFile, - newFileNeedExport: Set, - newFileNameWithExtension: string, + targetFileNeedExport: Set, + targetFileNameWithExtension: string, program: Program, host: LanguageServiceHost, useEs6Imports: boolean, @@ -506,15 +501,15 @@ export function createOldFileImportsFromNewFile( ): AnyImportOrRequireStatement | undefined { let defaultImport: Identifier | undefined; const imports: string[] = []; - newFileNeedExport.forEach(symbol => { + targetFileNeedExport.forEach(symbol => { if (symbol.escapedName === InternalSymbolName.Default) { - defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); // TODO: GH#18217 + defaultImport = factory.createIdentifier(symbolNameNoDefault(symbol)!); } else { imports.push(symbol.name); } }); - return makeImportOrRequire(sourceFile, defaultImport, imports, newFileNameWithExtension, program, host, useEs6Imports, quotePreference); + return makeImportOrRequire(sourceFile, defaultImport, imports, targetFileNameWithExtension, program, host, useEs6Imports, quotePreference); } /** @internal */ @@ -522,24 +517,24 @@ export function makeImportOrRequire( sourceFile: SourceFile, defaultImport: Identifier | undefined, imports: readonly string[], - newFileNameWithExtension: string, + targetFileNameWithExtension: 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)); + const pathToTargetFile = resolvePath(getDirectoryPath(sourceFile.path), targetFileNameWithExtension); + const pathToTargetFileWithCorrectExtension = getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.path, pathToTargetFile, 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); + return makeImportIfNecessary(defaultImport, specifiers, pathToTargetFileWithCorrectExtension, 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 + ? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(makeStringLiteral(pathToTargetFileWithCorrectExtension, quotePreference))) as RequireVariableStatement : undefined; } } @@ -819,12 +814,12 @@ export interface UsageInfo { readonly movedSymbols: Set; // Symbols declared in the old file that must be imported by the new file. (May not already be exported.) - readonly newFileImportsFromOldFile: Set; + readonly targetFileImportsFromOldFile: Set; // Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. - readonly oldFileImportsFromNewFile: Set; + readonly oldFileImportsFromTargetFile: Set; - readonly oldImportsNeededByNewFile: Map; - // Subset of oldImportsNeededByNewFile that are will no longer be used in the old file. + readonly oldImportsNeededByTargetFile: Map; + // Subset of oldImportsNeededByTargetFile that are will no longer be used in the old file. readonly unusedImportsFromOldFile: Set; } /** @internal */ @@ -848,7 +843,7 @@ export interface TopLevelVariableDeclaration extends VariableDeclaration { paren export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; /** @internal */ -export function createNewFilename(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { +export function createNewFileName(oldFile: SourceFile, program: Program, context: RefactorContext, host: LanguageServiceHost): string { const checker = program.getTypeChecker(); const toMove = getStatementsToMove(context); let usage; @@ -856,19 +851,19 @@ export function createNewFilename(oldFile: SourceFile, program: Program, context usage = getUsageInfo(oldFile, toMove.all, checker); const currentDirectory = getDirectoryPath(oldFile.fileName); const extension = extensionFromPath(oldFile.fileName); - const newFilename = combinePaths( + 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), + inferNewFileName(usage.oldFileImportsFromTargetFile, usage.movedSymbols), extension, currentDirectory, host)) // new file has same extension as old file + extension; - return newFilename; + return newFileName; } return ""; } @@ -939,14 +934,14 @@ function isPureImport(node: Node): boolean { /** @internal */ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo { const movedSymbols = new Set(); - const oldImportsNeededByNewFile = new Map(); - const newFileImportsFromOldFile = new Set(); + const oldImportsNeededByTargetFile = new Map(); + const targetFileImportsFromOldFile = new Set(); 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.set(jsxNamespaceSymbol, false); + oldImportsNeededByTargetFile.set(jsxNamespaceSymbol, false); } for (const statement of toMove) { @@ -959,33 +954,33 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], if (!symbol.declarations) return; for (const decl of symbol.declarations) { if (isInImport(decl)) { - const prevIsTypeOnly = oldImportsNeededByNewFile.get(symbol); - oldImportsNeededByNewFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); + const prevIsTypeOnly = oldImportsNeededByTargetFile.get(symbol); + oldImportsNeededByTargetFile.set(symbol, prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite); } else if (isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile && !movedSymbols.has(symbol)) { - newFileImportsFromOldFile.add(symbol); + targetFileImportsFromOldFile.add(symbol); } } }); } - const unusedImportsFromOldFile = new Set(oldImportsNeededByNewFile.keys()); + const unusedImportsFromOldFile = new Set(oldImportsNeededByTargetFile.keys()); - const oldFileImportsFromNewFile = new Set(); + const oldFileImportsFromTargetFile = new Set(); for (const statement of oldFile.statements) { if (contains(toMove, statement)) continue; - // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByNewFile. + // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByTargetFile. if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { unusedImportsFromOldFile.delete(jsxNamespaceSymbol); } forEachReference(statement, checker, symbol => { - if (movedSymbols.has(symbol)) oldFileImportsFromNewFile.add(symbol); + if (movedSymbols.has(symbol)) oldFileImportsFromTargetFile.add(symbol); unusedImportsFromOldFile.delete(symbol); }); } - return { movedSymbols, newFileImportsFromOldFile, oldFileImportsFromNewFile, oldImportsNeededByNewFile, unusedImportsFromOldFile }; + return { movedSymbols, targetFileImportsFromOldFile, oldFileImportsFromTargetFile, oldImportsNeededByTargetFile, unusedImportsFromOldFile }; function getJsxNamespaceSymbol(containsJsx: Node | undefined) { if (containsJsx === undefined) { @@ -1014,7 +1009,7 @@ function makeUniqueFilename(proposedFilename: string, extension: string, inDirec } } -function inferNewFilename(importsFromNewFile: Set, movedSymbols: Set): string { +function inferNewFileName(importsFromNewFile: Set, movedSymbols: Set): string { return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index 67daf4c37716a..10183ad20acc4 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -4,6 +4,7 @@ import { Debug, Diagnostics, emptyArray, + fileShouldUseJavaScriptRequire, getBaseFileName, getLocaleSpecificMessage, getQuotePreference, @@ -31,8 +32,8 @@ import { addExports, addExportToChanges, addNewFileToTsconfig, - createNewFilename, - createOldFileImportsFromNewFile, + createNewFileName, + createOldFileImportsFromTargetFile, deleteMovedStatements, deleteUnusedOldImports, filterImport, @@ -85,7 +86,7 @@ function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes const checker = program.getTypeChecker(); const usage = getUsageInfo(oldFile, toMove.all, checker); - const newFilename = createNewFilename(oldFile, program, context, host); + 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)); @@ -98,14 +99,14 @@ function getNewStatementsAndRemoveFromOldFile( ) { 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); } @@ -114,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, diff --git a/src/services/services.ts b/src/services/services.ts index 9906c6691f300..a1fbf1a732a8c 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -322,7 +322,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 { createNewFileName } from "./_namespaces/ts.refactor"; import * as classifier from "./classifier"; import * as classifier2020 from "./classifier2020"; @@ -2999,7 +2999,7 @@ export function createLanguageService( //creating new filename let newFileName; if (program) { - newFileName = createNewFilename(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); + newFileName = createNewFileName(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); } return { newFileName, files }; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e96f1c603556e..1b53697df64d4 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -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, @@ -4163,3 +4170,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; +} \ No newline at end of file diff --git a/tests/cases/fourslash/moveToFile_requireImport2.ts b/tests/cases/fourslash/moveToFile_requireImport2.ts new file mode 100644 index 0000000000000..ccf2b682fb515 --- /dev/null +++ b/tests/cases/fourslash/moveToFile_requireImport2.ts @@ -0,0 +1,41 @@ +/// + +// @verbatimModuleSyntax: true +// @module: commonjs +// @allowJs: true + +//@Filename: /a.js +////const { a, b } = require("./other"); +////const p = 0; +////[|const y = p; +////const z = 0; +////exports.z = 0;|] +////a; y; z; + +//@Filename: /other.js +////const a = 1; +////exports.a = a; + +//@Filename: /user.ts +////const { x, y } = require("./a"); + +verify.moveToFile({ + newFileContents: { + "/a.js": +`const { y, z } = require("./bar"); +const { a, b } = require("./other"); +const p = 0; +exports.p = p; +a; y; z;`, + + "/bar.js": +`const { p } = require("./a"); + +const y = p; +exports.y = y; +const z = 0; +exports.z = 0; +`, + }, + newFile: "/bar.js" +}); From f4e73aa09fd77faff8cfbf5591f6018b86bfdbfb Mon Sep 17 00:00:00 2001 From: navya9singh Date: Mon, 17 Apr 2023 12:42:13 -0700 Subject: [PATCH 36/42] Fixing baselines --- ...files,-excluding-node_modules-within-a-project.js | 6 ------ .../handles-moving-statement-to-an-existing-file.js | 12 ------------ 2 files changed, 18 deletions(-) diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js index 68bd5322cf7d0..3aac9f96385ce 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -57,13 +57,9 @@ Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/a/file4.ts 50 Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/b/file2.ts 500 undefined WatchType: Closed Script info Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /project/d/e/file3.ts 500 undefined WatchType: Closed Script info Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /project/tsconfig.json -Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations -Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined Project: /project/tsconfig.json WatchType: Failed Lookup Locations Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules 1 undefined WatchType: node_modules for closed script infos and package.jsons affecting module specifier cache Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /project/tsconfig.json WatchType: Missing file -Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots -Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /project/node_modules/@types 1 undefined Project: /project/tsconfig.json WatchType: Type roots Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /project/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info seq [hh:mm:ss:mss] Project '/project/tsconfig.json' (Configured) Info seq [hh:mm:ss:mss] Files (6) @@ -121,8 +117,6 @@ FsWatchesRecursive:: {} /project/node_modules: *new* {} -/project/node_modules/@types: *new* - {} Before request diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js index 367015e6bf171..6367d1721cd92 100644 --- a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -36,13 +36,7 @@ Info seq [hh:mm:ss:mss] Config: /Foo/tsconfig.json : { } Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /Foo/b.ts 500 undefined WatchType: Closed Script info Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /Foo/tsconfig.json -Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/bar 1 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations -Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo 0 undefined Project: /Foo/tsconfig.json WatchType: Failed Lookup Locations Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined Project: /Foo/tsconfig.json WatchType: Missing file -Info seq [hh:mm:ss:mss] DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots -Info seq [hh:mm:ss:mss] Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /Foo/node_modules/@types 1 undefined Project: /Foo/tsconfig.json WatchType: Type roots Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /Foo/tsconfig.json Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms Info seq [hh:mm:ss:mss] Project '/Foo/tsconfig.json' (Configured) Info seq [hh:mm:ss:mss] Files (2) @@ -70,20 +64,14 @@ Info seq [hh:mm:ss:mss] response: After request PolledWatches:: -/foo/bar: *new* - {"pollingInterval":500} /a/lib/lib.d.ts: *new* {"pollingInterval":500} -/foo/node_modules/@types: *new* - {"pollingInterval":500} FsWatches:: /foo/tsconfig.json: *new* {} /foo/b.ts: *new* {} -/foo: *new* - {} Before request From 38471a8861dd0bf793d75679d0ba1ee6ec4f4311 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Tue, 18 Apr 2023 13:42:22 -0700 Subject: [PATCH 37/42] adressing comments --- src/harness/client.ts | 2 +- src/harness/harnessLanguageService.ts | 2 +- src/server/protocol.ts | 2 +- src/server/session.ts | 4 +-- src/services/services.ts | 27 +++++-------------- src/services/types.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 6 ++--- tests/baselines/reference/api/typescript.d.ts | 4 +-- 8 files changed, 18 insertions(+), 31 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index fb633e2abaded..5d8b7134e63e5 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -793,7 +793,7 @@ export class SessionClient implements LanguageService { return response.body!; // TODO: GH#18217 } - getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string | undefined; files: string[] | undefined; } { + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange): { newFileName: string; files: string[]; } { const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); const request = this.processRequest(protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, args); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 5d48bc6bea496..b23af1ca6164a 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -619,7 +619,7 @@ class LanguageServiceShimProxy implements ts.LanguageService { getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } - getMoveToRefactoringFileSuggestions(): { newFileName: string | undefined, files: string[] | undefined } { + getMoveToRefactoringFileSuggestions(): { newFileName: string, files: string[] } { throw new Error("Not supported on the shim."); } organizeImports(_args: ts.OrganizeImportsArgs, _formatOptions: ts.FormatCodeSettings): readonly ts.FileTextChanges[] { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 587ad2993a311..98107b9647ba1 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -615,7 +615,7 @@ export type GetMoveToRefactoringFileSuggestionsRequestArgs = FileLocationOrRange * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring */ export interface GetMoveToRefactoringFileSuggestions extends Response { - body?: { + body: { newFileName: string; files: string[]; }; diff --git a/src/server/session.ts b/src/server/session.ts index f1846332d9c4f..5e4a7d5d06022 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2713,10 +2713,10 @@ export class Session implements EventSender { return result; } - private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFilename: string | undefined, files: string[] | undefined }{ + private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFilename: string, files: string[] }{ const { file, project } = this.getFileAndProject(args); const allFiles: string[] = []; - let fileName: string | undefined; + let fileName = ""; updateProjectIfDirty(project); const scriptInfo = project.getScriptInfoForNormalizedPath(file); if (scriptInfo) { diff --git a/src/services/services.ts b/src/services/services.ts index 72f5a18d64151..14f47929c1157 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2982,29 +2982,16 @@ export function createLanguageService( return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences, emptyOptions, triggerReason, kind)); } - function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFileName: string | undefined, files: string[] | undefined } { + function getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): { newFileName: string, files: string[] } { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); - const program = getProgram(); - const allFiles = program?.getSourceFiles().filter(sourceFile => !program?.isSourceFileFromExternalLibrary(sourceFile)).map(f => f.fileName); + const allFiles = Debug.checkDefined(program.getSourceFiles()); const extension = extensionFromPath(fileName); - const files: string[] = []; - if (allFiles) { - for (const file of allFiles) { - const fileExtension = extensionFromPath(file); - if (sourceFile === getValidSourceFile(file) || extension === Extension.Ts && fileExtension === Extension.Dts || extension === Extension.Dts && startsWith(getBaseFileName(file), "lib.") && fileExtension === Extension.Dts) { - continue; - } - if (extension === fileExtension) { - files.push(file); - } - } - } - //creating new filename - let newFileName; - if (program) { - newFileName = createNewFileName(sourceFile, program, getRefactorContext(sourceFile, positionOrRange, preferences, emptyOptions), host); - } + 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 }; } diff --git a/src/services/types.ts b/src/services/types.ts index 786eb1b616fde..8f244c9b6d265 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -631,7 +631,7 @@ export interface LanguageService { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; - getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFileName: string | undefined, files: string[] | undefined }; + getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { newFileName: string, files: string[] }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(args: OrganizeImportsArgs, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): readonly FileTextChanges[]; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 13c19011ac2c1..fd5cf1a679909 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -527,7 +527,7 @@ declare namespace ts { * Each refactoring exposes one or more "Actions"; a user selects one action to invoke a refactoring */ interface GetMoveToRefactoringFileSuggestions extends Response { - body?: { + body: { newFileName: string; files: string[]; }; @@ -10167,8 +10167,8 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { - newFileName: string | undefined; - files: string[] | undefined; + newFileName: string; + files: string[]; }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 8a2cb3235d811..d6d922eab32d9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6200,8 +6200,8 @@ declare namespace ts { applyCodeActionCommand(fileName: string, action: CodeActionCommand | CodeActionCommand[]): Promise; getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): ApplicableRefactorInfo[]; getMoveToRefactoringFileSuggestions(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined, triggerReason?: RefactorTriggerReason, kind?: string): { - newFileName: string | undefined; - files: string[] | undefined; + newFileName: string; + files: string[]; }; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; getEditsForMoveToFileRefactor(fileName: string, newFile: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; From 9734d4d005fdf062cb9131d42bea8ab608cc07e0 Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 20 Apr 2023 14:03:26 -0700 Subject: [PATCH 38/42] Changes for new api protocol --- src/harness/client.ts | 26 ------------- src/harness/fourslashImpl.ts | 12 +++--- src/harness/fourslashInterfaceImpl.ts | 2 +- src/harness/harnessLanguageService.ts | 3 -- src/server/protocol.ts | 19 ---------- src/server/session.ts | 38 ------------------- src/services/refactorProvider.ts | 11 ++---- src/services/refactors/moveToFile.ts | 11 ++++-- src/services/services.ts | 21 ++-------- src/services/types.ts | 12 ++++-- .../getMoveToRefactoringFileSuggestions.ts | 12 +++--- .../unittests/tsserver/refactors.ts | 26 ------------- .../reference/api/tsserverlibrary.d.ts | 24 ++++-------- tests/baselines/reference/api/typescript.d.ts | 10 ++++- tests/cases/fourslash/fourslash.ts | 5 ++- .../moveToFile_addImportFromTargetFile.ts | 2 +- .../fourslash/moveToFile_blankExistingFile.ts | 2 +- tests/cases/fourslash/moveToFile_ctsTomts.ts | 2 +- .../moveToFile_differentDirectories.ts | 2 +- .../moveToFile_differentDirectories2.ts | 2 +- .../fourslash/moveToFile_impossibleImport.ts | 2 +- .../moveToFile_multipleStatements.ts | 2 +- .../fourslash/moveToFile_namedImports.ts | 2 +- tests/cases/fourslash/moveToFile_newFile.ts | 2 +- .../fourslash/moveToFile_noImportExport.ts | 2 +- .../moveToFile_nonExportedImports.ts | 2 +- .../fourslash/moveToFile_requireImport.ts | 2 +- .../fourslash/moveToFile_requireImport2.ts | 2 +- .../cases/fourslash/moveToFile_typeImport.ts | 2 +- .../fourslash/moveToFile_unresolvedImport.ts | 2 +- .../fourslash/refactorKind_moveToNewFile.ts | 3 +- 31 files changed, 71 insertions(+), 194 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index 5d8b7134e63e5..b68f4791f4bdb 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -834,32 +834,6 @@ export class SessionClient implements LanguageService { }; } - getEditsForMoveToFileRefactor(fileName: string, _newFile: string, _formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, _preferences: UserPreferences | undefined): RefactorEditInfo { - const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; - args.refactor = refactorName; - args.action = actionName; - - const request = this.processRequest(protocol.CommandTypes.GetEditsForRefactor, args); - const response = this.processResponse(request); - - if (!response.body) { - return { edits: [], renameFilename: undefined, renameLocation: undefined }; - } - - const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); - - const renameFilename: string | undefined = response.body.renameFilename; - let renameLocation: number | undefined; - if (renameFilename !== undefined) { - renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); - } - return { - edits, - renameFilename, - renameLocation - }; - } - organizeImports(_args: OrganizeImportsArgs, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { return notImplemented(); } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 6ebf7781b6ee4..03d3b52afe7b3 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3908,12 +3908,12 @@ export class TestState { 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 }), r => r.name === "Move to file")!; + 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.getEditsForMoveToFileRefactor(range.fileName, options.newFile, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions)!; + 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); } @@ -4222,11 +4222,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 { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 867a1074dc96c..7bb2d379130a3 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1902,7 +1902,7 @@ export interface MoveToNewFileOptions { export interface MoveToFileOptions { readonly newFileContents: { readonly [fileName: string]: string }; - readonly newFile: string; + readonly interactiveRefactorArguments: ts.InteractiveRefactorArguments; readonly preferences?: ts.UserPreferences; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b23af1ca6164a..448d3a6f4bc44 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -613,9 +613,6 @@ class LanguageServiceShimProxy implements ts.LanguageService { getEditsForRefactor(): ts.RefactorEditInfo { throw new Error("Not supported on the shim."); } - getEditsForMoveToFileRefactor(): ts.RefactorEditInfo { - throw new Error("Not supported on the shim."); - } getApplicableRefactors(): ts.ApplicableRefactorInfo[] { throw new Error("Not supported on the shim."); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 8fe8dd9a101f6..10d099a22319f 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -142,7 +142,6 @@ export const enum CommandTypes { GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", - GetEditsForMoveToFileRefactor = "getEditsForMoveToFileRefactor", GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", /** @internal */ GetEditsForRefactorFull = "getEditsForRefactor-full", @@ -710,24 +709,6 @@ export interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; } -export interface GetEditsForMoveToFileRefactorRequest extends Request { - command: CommandTypes.GetEditsForMoveToFileRefactor; - arguments: GetEditsForMoveToFileRefactorRequestArgs; -} - -export interface GetEditsForMoveToFileRefactorResponse extends Response { - body?: RefactorEditInfo; // TODO: maybe use a new type -} - -export type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeRequestArgs & { - /* The 'name' property from the refactoring that offered this action */ - refactor: string; - /* The 'name' property from the refactoring action */ - action: string; - /* Target file path that the user selected (may also be new file) */ - targetFile: string; -}; - export interface RefactorEditInfo { edits: FileCodeEdits[]; diff --git a/src/server/session.ts b/src/server/session.ts index 8f601821113c4..010e1de0925ec 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -884,7 +884,6 @@ const invalidPartialSemanticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.GetApplicableRefactors, protocol.CommandTypes.GetMoveToRefactoringFileSuggestions, protocol.CommandTypes.GetEditsForRefactor, - protocol.CommandTypes.GetEditsForMoveToFileRefactor, protocol.CommandTypes.GetEditsForRefactorFull, protocol.CommandTypes.OrganizeImports, protocol.CommandTypes.OrganizeImportsFull, @@ -2734,40 +2733,6 @@ export class Session implements EventSender { return { newFilename: fileName, files: allFiles }; } - private getEditsForMoveToFileForRefactor(args: protocol.GetEditsForMoveToFileRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { - const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; - const result = project.getLanguageService().getEditsForMoveToFileRefactor( - file, - args.targetFile, - this.getFormatOptions(file), - this.extractPositionOrRange(args, scriptInfo), - args.refactor, - args.action, - this.getPreferences(file), - ); - - if (result === undefined) { - return { - edits: [] - }; - } - - if (simplifiedResult) { - const { renameFilename, renameLocation, edits } = result; - let mappedRenameLocation: protocol.Location | undefined; - if (renameFilename !== undefined && renameLocation !== undefined) { - const renameScriptInfo = project.getScriptInfoForNormalizedPath(toNormalizedPath(renameFilename))!; - mappedRenameLocation = getLocationInNewDocument(getSnapshotText(renameScriptInfo.getSnapshot()), renameFilename, renameLocation, edits); - } - return { - renameLocation: mappedRenameLocation, - renameFilename, - edits: this.mapTextChangesToCodeEdits(edits) }; - } - return result; - } - private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { Debug.assert(args.scope.type === "file"); const { file, project } = this.getFileAndProject(args.scope.args); @@ -3488,9 +3453,6 @@ export class Session implements EventSender { [protocol.CommandTypes.GetEditsForRefactor]: (request: protocol.GetEditsForRefactorRequest) => { return this.requiredResponse(this.getEditsForRefactor(request.arguments, /*simplifiedResult*/ true)); }, - [protocol.CommandTypes.GetEditsForMoveToFileRefactor]: (request: protocol.GetEditsForMoveToFileRefactorRequest) => { - return this.requiredResponse(this.getEditsForMoveToFileForRefactor(request.arguments, /*simplifiedResult*/ true)); - }, [protocol.CommandTypes.GetMoveToRefactoringFileSuggestions]: (request: protocol.GetMoveToRefactoringFileSuggestionsRequest) => { return this.requiredResponse(this.getMoveToRefactoringFileSuggestions(request.arguments)); }, diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index dcdc9f6ce6df2..a540be35b511b 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -2,6 +2,7 @@ import { ApplicableRefactorInfo, arrayFrom, flatMapIterator, + InteractiveRefactorArguments, Refactor, RefactorContext, RefactorEditInfo, @@ -30,13 +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); -} - -/** @internal */ -export function getEditsForMoveToFileRefactor(context: RefactorContext, newFile: string, refactorName: string, actionName: string): RefactorEditInfo | undefined { - const refactor = refactors.get(refactorName); - return refactor && refactor.getEditsForAction(context, actionName, newFile); + return refactor && refactor.getEditsForAction(context, actionName, interactiveRefactorArguments); } diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 1a6cf1aac3b6f..20ee5c05647d1 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -143,8 +143,11 @@ const moveToFileAction = { }; registerRefactor(refactorNameForMoveToFile, { kinds: [moveToFileAction.kind], - getAvailableActions: function getRefactorActionsToMoveToFile(context): readonly ApplicableRefactorInfo[] { + getAvailableActions: function getRefactorActionsToMoveToFile(context, interactiveRefactorArguments): readonly ApplicableRefactorInfo[] { const statements = getStatementsToMove(context); + if (!interactiveRefactorArguments) { + return emptyArray; + } if (context.preferences.allowTextChangesInNewFiles && statements) { return [{ name: refactorNameForMoveToFile, description, actions: [moveToFileAction] }]; } @@ -155,11 +158,11 @@ registerRefactor(refactorNameForMoveToFile, { } return emptyArray; }, - getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, targetFile): RefactorEditInfo | undefined { + getEditsForAction: function getRefactorEditsToMoveToFile(context, actionName, interactiveRefactorArguments): RefactorEditInfo | undefined { Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); const statements = Debug.checkDefined(getStatementsToMove(context)); - Debug.assert(targetFile, "Target file does not exist"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, targetFile, context.program, statements, t, context.host, context.preferences)); + Debug.assert(interactiveRefactorArguments, "No interactive refactor arguments available"); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context, context.file, interactiveRefactorArguments.targetFile, context.program, statements, t, context.host, context.preferences)); return { edits, renameFilename: undefined, renameLocation: undefined }; } }); diff --git a/src/services/services.ts b/src/services/services.ts index df2338feae378..80e88ced67e49 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -137,6 +137,7 @@ import { InlayHints, InlayHintsContext, insertSorted, + InteractiveRefactorArguments, InterfaceType, IntersectionType, isArray, @@ -3002,24 +3003,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); - } - - function getEditsForMoveToFileRefactor( - fileName: string, - newFile: string, - formatOptions: FormatCodeSettings, - positionOrRange: number | TextRange, - refactorName: string, - actionName: string, - preferences: UserPreferences = emptyOptions, - ): RefactorEditInfo | undefined { - synchronizeHostData(); - const file = getValidSourceFile(fileName); - return refactor.getEditsForMoveToFileRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), newFile, refactorName, actionName); + return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName, interactiveRefactorArguments); } function toLineColumnOffset(fileName: string, position: number): LineAndCharacter { @@ -3115,9 +3103,8 @@ export function createLanguageService( getAutoImportProvider, updateIsDefinitionOfReferencedSymbols, getApplicableRefactors, - getMoveToRefactoringFileSuggestions, getEditsForRefactor, - getEditsForMoveToFileRefactor, + getMoveToRefactoringFileSuggestions, toLineColumnOffset, getSourceMapper: () => sourceMapper, clearSourceMapperCache: () => sourceMapper.clearCache(), diff --git a/src/services/types.ts b/src/services/types.ts index cf4dc55194edf..6898bd20cadba 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -637,8 +637,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; - getEditsForMoveToFileRefactor(fileName: string, newFile: 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[]; @@ -1279,6 +1279,10 @@ export interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } +export interface InteractiveRefactorArguments { + targetFile: string; +} + export interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; @@ -1771,10 +1775,10 @@ export interface Refactor { kinds?: string[]; /** Compute the associated code actions */ - getEditsForAction(context: RefactorContext, actionName: string, newFile?: 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 */ diff --git a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts index bc05c8b41c035..e70ab769cb052 100644 --- a/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts +++ b/src/testRunner/unittests/tsserver/getMoveToRefactoringFileSuggestions.ts @@ -1,14 +1,14 @@ import * as ts from "../../_namespaces/ts"; -import { - createServerHost, - File, -} from "../virtualFileSystemWithWatch"; import { baselineTsserverLogs, createLoggerWithInMemoryLogs, createSession, - openFilesForSession, -} from "./helpers"; + 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", () => { diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index df883a16f08d3..6b38ba34878d0 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -94,30 +94,4 @@ 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({ - command: ts.server.protocol.CommandTypes.GetEditsForMoveToFileRefactor, - arguments: { - file: aTs.path, - startLine: 1, - startOffset: 1, - endLine: 2, - endOffset: aTs.content.length, - refactor: "Move to file", - action: "Move to file", - targetFile: "/Foo/b.ts", - } - }); - baselineTsserverLogs("refactors", "handles moving statement to an existing file", session); - }); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 3355c7f569ea5..f468c376c22fd 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -155,7 +155,6 @@ declare namespace ts { GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", - GetEditsForMoveToFileRefactor = "getEditsForMoveToFileRefactor", GetMoveToRefactoringFileSuggestions = "getMoveToRefactoringFileSuggestions", OrganizeImports = "organizeImports", GetEditsForFileRename = "getEditsForFileRename", @@ -608,18 +607,6 @@ declare namespace ts { interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; } - interface GetEditsForMoveToFileRefactorRequest extends Request { - command: CommandTypes.GetEditsForMoveToFileRefactor; - arguments: GetEditsForMoveToFileRefactorRequestArgs; - } - interface GetEditsForMoveToFileRefactorResponse extends Response { - body?: RefactorEditInfo; - } - type GetEditsForMoveToFileRefactorRequestArgs = FileLocationOrRangeRequestArgs & { - refactor: string; - action: string; - targetFile: string; - }; interface RefactorEditInfo { edits: FileCodeEdits[]; /** @@ -4024,7 +4011,6 @@ declare namespace ts { private getApplicableRefactors; private getEditsForRefactor; private getMoveToRefactoringFileSuggestions; - private getEditsForMoveToFileForRefactor; private organizeImports; private getEditsForFileRename; private getCodeFixes; @@ -10185,8 +10171,11 @@ declare namespace ts { * 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; - getEditsForMoveToFileRefactor(fileName: string, newFile: 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[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; @@ -10694,6 +10683,9 @@ declare namespace ts { interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index be7e1605caf2a..a7d30d6c3e1f5 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6205,8 +6205,11 @@ declare namespace ts { * 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; - getEditsForMoveToFileRefactor(fileName: string, newFile: 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[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): EmitOutput; @@ -6714,6 +6717,9 @@ declare namespace ts { interface DocCommentTemplateOptions { readonly generateReturnInDocTemplate?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface SignatureHelpParameter { name: string; documentation: SymbolDisplayPart[]; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index ff938d9eb99dd..9cf274e618978 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -198,6 +198,9 @@ declare namespace FourSlashInterface { readonly semicolons?: ts.SemicolonPreference; readonly indentSwitchCase?: boolean; } + interface InteractiveRefactorArguments { + targetFile: string; + } interface Range { fileName: string; pos: number; @@ -433,7 +436,7 @@ declare namespace FourSlashInterface { noMoveToNewFile(): void; moveToFile(options: { readonly newFileContents: { readonly [fileName: string]: string }; - readonly newFile: string; + readonly interactiveRefactorArguments: InteractiveRefactorArguments; readonly preferences?: UserPreferences; }): void; generateTypes(...options: GenerateTypesOptions[]): void; diff --git a/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts index 5c9ca10795fe4..f49d27744bc07 100644 --- a/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts +++ b/tests/cases/fourslash/moveToFile_addImportFromTargetFile.ts @@ -29,5 +29,5 @@ const a = 2; export const y = b + 10; `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: {targetFile: "/bar.ts"}, }); diff --git a/tests/cases/fourslash/moveToFile_blankExistingFile.ts b/tests/cases/fourslash/moveToFile_blankExistingFile.ts index 9a96731711356..dda33fca96f55 100644 --- a/tests/cases/fourslash/moveToFile_blankExistingFile.ts +++ b/tests/cases/fourslash/moveToFile_blankExistingFile.ts @@ -24,5 +24,5 @@ import { p } from './a'; const y: Date = p + b; `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: {targetFile: "/bar.ts"}, }); diff --git a/tests/cases/fourslash/moveToFile_ctsTomts.ts b/tests/cases/fourslash/moveToFile_ctsTomts.ts index 250ddcbe3c9b4..2a911fa05c7c0 100644 --- a/tests/cases/fourslash/moveToFile_ctsTomts.ts +++ b/tests/cases/fourslash/moveToFile_ctsTomts.ts @@ -32,5 +32,5 @@ const a = 2; export const y = p + b; `, }, - newFile: "/src/dir2/bar.mts", + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.mts" }, }); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories.ts b/tests/cases/fourslash/moveToFile_differentDirectories.ts index 4c1faa4ee7b25..2c9a1c544c116 100644 --- a/tests/cases/fourslash/moveToFile_differentDirectories.ts +++ b/tests/cases/fourslash/moveToFile_differentDirectories.ts @@ -28,5 +28,5 @@ const a = 2; export const y = b + 10; `, }, - newFile: "/src/dir2/bar.ts", + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.ts" }, }); diff --git a/tests/cases/fourslash/moveToFile_differentDirectories2.ts b/tests/cases/fourslash/moveToFile_differentDirectories2.ts index c317761a99f3a..4dba2881f189c 100644 --- a/tests/cases/fourslash/moveToFile_differentDirectories2.ts +++ b/tests/cases/fourslash/moveToFile_differentDirectories2.ts @@ -29,5 +29,5 @@ import { a } from '../dir1/a'; export const y = b + a; `, }, - newFile: "/src/dir2/bar.ts", + interactiveRefactorArguments: { targetFile: "/src/dir2/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_impossibleImport.ts b/tests/cases/fourslash/moveToFile_impossibleImport.ts index c3ddfaf1aeef0..08f8b5e38a5a3 100644 --- a/tests/cases/fourslash/moveToFile_impossibleImport.ts +++ b/tests/cases/fourslash/moveToFile_impossibleImport.ts @@ -33,5 +33,5 @@ verify.moveToFile({ esm.ohno; `, }, - newFile: "/bar.cts", + interactiveRefactorArguments: { targetFile: "/bar.cts" } }); \ No newline at end of file diff --git a/tests/cases/fourslash/moveToFile_multipleStatements.ts b/tests/cases/fourslash/moveToFile_multipleStatements.ts index 2286be7b16c53..af2454d59d280 100644 --- a/tests/cases/fourslash/moveToFile_multipleStatements.ts +++ b/tests/cases/fourslash/moveToFile_multipleStatements.ts @@ -32,5 +32,5 @@ type T = number; interface I { } `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_namedImports.ts b/tests/cases/fourslash/moveToFile_namedImports.ts index 8df7383dda501..916831d7699fd 100644 --- a/tests/cases/fourslash/moveToFile_namedImports.ts +++ b/tests/cases/fourslash/moveToFile_namedImports.ts @@ -37,7 +37,7 @@ const q = 0; const y = p + b; `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" }, preferences: { quotePreference: "single", diff --git a/tests/cases/fourslash/moveToFile_newFile.ts b/tests/cases/fourslash/moveToFile_newFile.ts index 54bbcae350dec..ae8ed5bfe28dc 100644 --- a/tests/cases/fourslash/moveToFile_newFile.ts +++ b/tests/cases/fourslash/moveToFile_newFile.ts @@ -16,5 +16,5 @@ verify.moveToFile({ } `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_noImportExport.ts b/tests/cases/fourslash/moveToFile_noImportExport.ts index 548d95d607639..bd992fad71293 100644 --- a/tests/cases/fourslash/moveToFile_noImportExport.ts +++ b/tests/cases/fourslash/moveToFile_noImportExport.ts @@ -23,5 +23,5 @@ const q = "test"; export const y = z + 2; `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_nonExportedImports.ts b/tests/cases/fourslash/moveToFile_nonExportedImports.ts index 63a30480dd917..142505c3e2636 100644 --- a/tests/cases/fourslash/moveToFile_nonExportedImports.ts +++ b/tests/cases/fourslash/moveToFile_nonExportedImports.ts @@ -28,5 +28,5 @@ const q = 20; const y: Date = p + b; `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_requireImport.ts b/tests/cases/fourslash/moveToFile_requireImport.ts index d3adb9e19173a..df6a294af2155 100644 --- a/tests/cases/fourslash/moveToFile_requireImport.ts +++ b/tests/cases/fourslash/moveToFile_requireImport.ts @@ -40,5 +40,5 @@ const z = 0; exports.z = 0; `, }, - newFile: "/bar.js" + interactiveRefactorArguments: { targetFile: "/bar.js" } }); diff --git a/tests/cases/fourslash/moveToFile_requireImport2.ts b/tests/cases/fourslash/moveToFile_requireImport2.ts index ccf2b682fb515..a0e5ffd8226ad 100644 --- a/tests/cases/fourslash/moveToFile_requireImport2.ts +++ b/tests/cases/fourslash/moveToFile_requireImport2.ts @@ -37,5 +37,5 @@ const z = 0; exports.z = 0; `, }, - newFile: "/bar.js" + interactiveRefactorArguments: { targetFile: "/bar.js" } }); diff --git a/tests/cases/fourslash/moveToFile_typeImport.ts b/tests/cases/fourslash/moveToFile_typeImport.ts index 672015eeb7c77..61e1b2b48e2c0 100644 --- a/tests/cases/fourslash/moveToFile_typeImport.ts +++ b/tests/cases/fourslash/moveToFile_typeImport.ts @@ -31,5 +31,5 @@ import {} from "./somefile"; function f(a: B) { } `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); diff --git a/tests/cases/fourslash/moveToFile_unresolvedImport.ts b/tests/cases/fourslash/moveToFile_unresolvedImport.ts index 24b842cea42ea..8cdd873361f22 100644 --- a/tests/cases/fourslash/moveToFile_unresolvedImport.ts +++ b/tests/cases/fourslash/moveToFile_unresolvedImport.ts @@ -19,5 +19,5 @@ const a = 1; a(); `, }, - newFile: "/bar.ts", + interactiveRefactorArguments: { targetFile: "/bar.ts" } }); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorKind_moveToNewFile.ts b/tests/cases/fourslash/refactorKind_moveToNewFile.ts index 7d039727e3f5e..d9b280f565222 100644 --- a/tests/cases/fourslash/refactorKind_moveToNewFile.ts +++ b/tests/cases/fourslash/refactorKind_moveToNewFile.ts @@ -5,8 +5,7 @@ goTo.select("a", "b"); verify.refactorKindAvailable("refactor.move", [ - "refactor.move.newFile", - "refactor.move.file" + "refactor.move.newFile" ], { allowTextChangesInNewFiles: true From 003113c08f60dd897d3b89541c3870bb249d6d6c Mon Sep 17 00:00:00 2001 From: navya9singh Date: Thu, 20 Apr 2023 16:09:35 -0700 Subject: [PATCH 39/42] fixing tests --- src/server/protocol.ts | 3 +++ src/server/session.ts | 1 + .../unittests/tsserver/refactors.ts | 26 +++++++++++++++++++ ...es-moving-statement-to-an-existing-file.js | 6 +++-- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 10d099a22319f..4e4432d0bc2e7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -4,6 +4,7 @@ import type { EndOfLineState, FileExtensionInfo, HighlightSpanKind, + InteractiveRefactorArguments, MapLike, OutliningSpanKind, OutputFile, @@ -702,6 +703,8 @@ export type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & { refactor: string; /* The 'name' property from the refactoring action */ action: string; + /* Arguments for interactive action */ + interactiveRefactorArguments?: InteractiveRefactorArguments; }; diff --git a/src/server/session.ts b/src/server/session.ts index 010e1de0925ec..c9b1643bfabf3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2689,6 +2689,7 @@ export class Session implements EventSender { args.refactor, args.action, this.getPreferences(file), + args.interactiveRefactorArguments ); if (result === undefined) { diff --git a/src/testRunner/unittests/tsserver/refactors.ts b/src/testRunner/unittests/tsserver/refactors.ts index 6b38ba34878d0..49ae2f77aeb57 100644 --- a/src/testRunner/unittests/tsserver/refactors.ts +++ b/src/testRunner/unittests/tsserver/refactors.ts @@ -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({ + 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); + }); }); diff --git a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js index 6367d1721cd92..8f6e4a458ba4a 100644 --- a/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js +++ b/tests/baselines/reference/tsserver/refactors/handles-moving-statement-to-an-existing-file.js @@ -77,7 +77,7 @@ Before request Info seq [hh:mm:ss:mss] request: { - "command": "getEditsForMoveToFileRefactor", + "command": "getEditsForRefactor", "arguments": { "file": "/Foo/a.ts", "startLine": 1, @@ -86,7 +86,9 @@ Info seq [hh:mm:ss:mss] request: "endOffset": 12, "refactor": "Move to file", "action": "Move to file", - "targetFile": "/Foo/b.ts" + "interactiveRefactorArguments": { + "targetFile": "/Foo/b.ts" + } }, "seq": 2, "type": "request" From 28d8564f910bada17c805a5a3858c78324b96112 Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Fri, 21 Apr 2023 16:36:24 +0000 Subject: [PATCH 40/42] accepting baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index f468c376c22fd..5b45c6d0b58f0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -603,6 +603,7 @@ declare namespace ts { type GetEditsForRefactorRequestArgs = FileLocationOrRangeRequestArgs & { refactor: string; action: string; + interactiveRefactorArguments?: InteractiveRefactorArguments; }; interface GetEditsForRefactorResponse extends Response { body?: RefactorEditInfo; From e61ad6d1e537951994691d1af07394b6ea1ee2ca Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Fri, 21 Apr 2023 17:52:49 +0000 Subject: [PATCH 41/42] Simplifying fileSuggestions in session.ts --- src/server/session.ts | 21 ++++--------------- .../skips-lib.d.ts-files.js | 2 +- ...ggests-only-.js-file-for-a-.js-filepath.js | 2 +- ...ggests-only-.ts-file-for-a-.ts-filepath.js | 2 +- ...excluding-node_modules-within-a-project.js | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index c9b1643bfabf3..8fc1bdb5c62fd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2714,24 +2714,11 @@ export class Session implements EventSender { return result; } - private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFilename: string, files: string[] }{ + private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFileName: string, files: string[] }{ const { file, project } = this.getFileAndProject(args); - const allFiles: string[] = []; - let fileName = ""; - updateProjectIfDirty(project); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - if (scriptInfo) { - const { newFileName, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); - if (files) { - for (const file of files) { - if (!allFiles.includes(file)) { - allFiles.push(file); - } - } - } - fileName = newFileName; - } - return { newFilename: fileName, files: allFiles }; + const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; + const { newFileName, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); + return { newFileName, files }; } private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] { diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js index c8936ee373e3d..5f107bc89a04e 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/skips-lib.d.ts-files.js @@ -108,7 +108,7 @@ Info seq [hh:mm:ss:mss] request: Info seq [hh:mm:ss:mss] response: { "response": { - "newFilename": "/C.d.ts", + "newFileName": "/C.d.ts", "files": [ "/a/file3.d.ts" ] diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js index ceaa602719133..0f9fe4cd95541 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.js-file-for-a-.js-filepath.js @@ -118,7 +118,7 @@ Info seq [hh:mm:ss:mss] request: Info seq [hh:mm:ss:mss] response: { "response": { - "newFilename": "/C.js", + "newFileName": "/C.js", "files": [ "/file2.js", "/file5.js" diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js index ced34f1ef2725..656ad93ccbd26 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/suggests-only-.ts-file-for-a-.ts-filepath.js @@ -141,7 +141,7 @@ Info seq [hh:mm:ss:mss] request: Info seq [hh:mm:ss:mss] response: { "response": { - "newFilename": "/ka.ts", + "newFileName": "/ka.ts", "files": [ "/file7.ts" ] diff --git a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js index 3aac9f96385ce..4b7ac756e01ad 100644 --- a/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js +++ b/tests/baselines/reference/tsserver/getMoveToRefactoringFileSuggestions/works-for-suggesting-a-list-of-files,-excluding-node_modules-within-a-project.js @@ -134,7 +134,7 @@ Info seq [hh:mm:ss:mss] request: Info seq [hh:mm:ss:mss] response: { "response": { - "newFilename": "/project/a/ka.ts", + "newFileName": "/project/a/ka.ts", "files": [ "/project/a/file4.ts", "/project/b/file2.ts", From ea6441d3d77963b1c692c8aca023a822e039dc67 Mon Sep 17 00:00:00 2001 From: navya9singh <108360753+navya9singh@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:26:01 +0000 Subject: [PATCH 42/42] Fixing return statement --- src/server/session.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 8fc1bdb5c62fd..b2369f8b8ebc3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2717,8 +2717,7 @@ export class Session implements EventSender { private getMoveToRefactoringFileSuggestions(args: protocol.GetMoveToRefactoringFileSuggestionsRequestArgs): { newFileName: string, files: string[] }{ const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file)!; - const { newFileName, files } = project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); - return { newFileName, files }; + return project.getLanguageService().getMoveToRefactoringFileSuggestions(file, this.extractPositionOrRange(args, scriptInfo), this.getPreferences(file)); } private organizeImports(args: protocol.OrganizeImportsRequestArgs, simplifiedResult: boolean): readonly protocol.FileCodeEdits[] | readonly FileTextChanges[] {