diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8e5c03560db3e..bccae5ac545e2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27657,6 +27657,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return Diagnostics.No_value_exists_in_scope_for_the_shorthand_property_0_Either_declare_one_or_provide_an_initializer; } else { + // Check if there's a type symbol with the same name + const typeSymbol = resolveName(node, node.escapedText, SymbolFlags.Type, /*nameNotFoundMessage*/ undefined, /*isUse*/ false); + if (typeSymbol && !(typeSymbol.flags & SymbolFlags.Value)) { + return Diagnostics.Cannot_find_name_0_1_only_refers_to_a_type_but_is_being_used_as_a_value_here; + } return Diagnostics.Cannot_find_name_0; } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index c21fd11e5c7bf..2c3bf60547f2d 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2840,6 +2840,10 @@ "category": "Error", "code": 2552 }, + "Cannot find name '{0}'. '{1}' only refers to a type, but is being used as a value here.": { + "category": "Error", + "code": 2888 + }, "Computed values are not permitted in an enum with string valued members.": { "category": "Error", "code": 2553 diff --git a/src/services/refactors/moveToFile.ts b/src/services/refactors/moveToFile.ts index 603f049a0ddc7..3b710389e0f13 100644 --- a/src/services/refactors/moveToFile.ts +++ b/src/services/refactors/moveToFile.ts @@ -1,169 +1,173 @@ import { ApplicableRefactorInfo, - arrayFrom, - AssignmentDeclarationKind, - BinaryExpression, - BindingElement, - BindingName, - CallExpression, - canHaveDecorators, - canHaveModifiers, - canHaveSymbol, - cast, - ClassDeclaration, - codefix, - combinePaths, - Comparison, - concatenate, - contains, - createFutureSourceFile, - createModuleSpecifierResolutionHost, - createTextRangeFromSpan, - Debug, - Declaration, - DeclarationStatement, - Diagnostics, - emptyArray, - EnumDeclaration, - escapeLeadingUnderscores, - every, - ExportDeclaration, - ExportKind, - Expression, - ExpressionStatement, - extensionFromPath, - ExternalModuleReference, - factory, - fileShouldUseJavaScriptRequire, - filter, - find, - FindAllReferences, - findAncestor, - findIndex, - findLast, - firstDefined, - flatMap, - forEachKey, - FunctionDeclaration, - FutureSourceFile, - getAssignmentDeclarationKind, - GetCanonicalFileName, - getDecorators, - getDirectoryPath, - getEmitScriptTarget, getLineAndCharacterOfPosition, getLocaleSpecificMessage, - getModifiers, - getNameForExportedSymbol, - getNormalizedAbsolutePath, - getPropertySymbolFromBindingElement, - getQuotePreference, - getRangesWhere, - getRefactorContextSpan, - getRelativePathFromFile, - getSourceFileOfNode, - getStringComparer, - getSynthesizedDeepClone, + last, + Node, + isBlock, + isIfStatement, + isIdentifier, + isStatement, + SyntaxKind, + TextRange, + RefactorContext, + RefactorEditInfo, + Debug, getTokenAtPosition, - getUniqueName, + findAncestor, + getSynthesizedDeepClone, hasJSFileExtension, - hasSyntacticModifier, hasTSFileExtension, + getRelativePathFromFile, hostGetCanonicalFileName, - Identifier, - ImportDeclaration, - ImportEqualsDeclaration, - importFromModuleSpecifier, - InterfaceDeclaration, - isArrayLiteralExpression, - isBinaryExpression, - isBindingElement, - isBlockLike, - isDeclarationName, - isExportDeclaration, - isExportSpecifier, - isExpressionStatement, - isExternalModuleReference, + symbolNameNoDefault, + ScriptTarget, + createFutureSourceFile, + isSourceFile, + SourceFile, + isVariableStatement, + emptyArray, + Diagnostics, + textChanges, + Statement, + Program, + LanguageServiceHost, + UserPreferences, + TypeChecker, + FutureSourceFile, + codefix, + ModuleKind, + isPrologueDirective, + getQuotePreference, + QuotePreference, + GetCanonicalFileName, isFullSourceFile, - isFunctionLikeDeclaration, - isIdentifier, - isImportClause, + ObjectBindingElementWithoutPropertyName, + getPropertySymbolFromBindingElement, + fileShouldUseJavaScriptRequire, + factory, + moduleSpecifierToValidIdentifier, + Symbol, + Identifier, isImportDeclaration, isImportEqualsDeclaration, - isImportSpecifier, - isNamedExports, - isNamedImports, - isNamespaceImport, - isObjectBindingPattern, - isObjectLiteralExpression, - isOmittedExpression, - isPrologueDirective, - isPropertyAccessExpression, - isPropertyAssignment, - isRequireCall, - isSourceFile, - isStatement, + isExternalModuleReference, isStringLiteral, isStringLiteralLike, - isValidTypeOnlyAliasUseSite, - isVariableDeclaration, - isVariableDeclarationInitializedToRequire, - isVariableStatement, - LanguageServiceHost, - last, - length, + isRequireCall, + isBindingElement, + contains, + skipAlias, + isPropertyAccessExpression, + SymbolFlags, + FindAllReferences, + Comparison, + getStringComparer, + resolvePath, + getDirectoryPath, + getNormalizedAbsolutePath, + moduleSpecifiers, + createModuleSpecifierResolutionHost, makeStringLiteral, - mapDefined, + getUniqueName, + tryCast, + isObjectLiteralExpression, + PropertyAssignment, + isPropertyAssignment, + isArrayLiteralExpression, + find, + normalizePath, + combinePaths, + nodeSeenTracker, + isExpressionStatement, + hasSyntacticModifier, ModifierFlags, + escapeLeadingUnderscores, + BindingName, + TypeNode, + Expression, + NodeFlags, + flatMap, + canHaveSymbol, + canHaveModifiers, + concatenate, + getModifiers, + canHaveDecorators, + getDecorators, ModifierLike, - ModuleDeclaration, - ModuleKind, - moduleSpecifiers, - moduleSpecifierToValidIdentifier, + DeclarationStatement, + mapDefined, + StringLiteralLike, NamedImportBindings, - Node, - NodeFlags, - nodeSeenTracker, - normalizePath, - ObjectBindingElementWithoutPropertyName, - Program, - PropertyAccessExpression, - PropertyAssignment, - QuotePreference, - rangeContainsRange, - RefactorContext, - RefactorEditInfo, + isVariableDeclaration, + VariableDeclaration, + VariableDeclarationList, + CallExpression, + cast, + ImportDeclaration, + ImportEqualsDeclaration, + VariableStatement, + ExternalModuleReference, RequireOrImportCall, - resolvePath, - ScriptTarget, - skipAlias, - some, - SourceFile, - Statement, - StringLiteralLike, - Symbol, - SymbolFlags, - symbolNameNoDefault, - SyntaxKind, + getNameForExportedSymbol, + getEmitScriptTarget, + ExportKind, takeWhile, - textChanges, - TextRange, + rangeContainsRange, + isImportSpecifier, + isImportClause, + isNamespaceImport, + some, + every, TransformFlags, - tryCast, + isDeclarationName, + isValidTypeOnlyAliasUseSite, + findIndex, + arrayFrom, + ClassDeclaration, + FunctionDeclaration, + EnumDeclaration, TypeAliasDeclaration, - TypeChecker, - TypeNode, - UserPreferences, - VariableDeclaration, - VariableDeclarationList, - VariableStatement, + InterfaceDeclaration, + ModuleDeclaration, + BinaryExpression, + PropertyAccessExpression, + Declaration, + getRefactorContextSpan, + createTextRangeFromSpan, + forEachKey, + firstDefined, + findLast, + isExportDeclaration, + isExportSpecifier, + isNamedExports, + isNamedImports, + isObjectBindingPattern, + isVariableDeclarationInitializedToRequire, + getSourceFileOfNode, + isFunctionLikeDeclaration, + isBinaryExpression, + getAssignmentDeclarationKind, + AssignmentDeclarationKind, + isOmittedExpression, + length, + BindingElement, + ExpressionStatement, + ExportDeclaration, + extensionFromPath, + importFromModuleSpecifier, + filter, } from "../_namespaces/ts.js"; + import { addTargetFileImports, registerRefactor, } from "../_namespaces/ts.refactor.js"; +// NOTE: we do NOT import doChange from ts.refactor; we provide our own safe implementation below. + const refactorNameForMoveToFile = "Move to file"; + const description = getLocaleSpecificMessage(Diagnostics.Move_to_file); const moveToFileAction = { @@ -171,67 +175,274 @@ const moveToFileAction = { description, kind: "refactor.move.file", }; + registerRefactor(refactorNameForMoveToFile, { kinds: [moveToFileAction.kind], - getAvailableActions: function getRefactorActionsToMoveToFile(context, interactiveRefactorArguments): readonly ApplicableRefactorInfo[] { + getAvailableActions: function getRefactorActionsToMoveToFile( + context: RefactorContext, + interactiveRefactorArguments + ): readonly ApplicableRefactorInfo[] { const file = context.file; + + // Use robust statement detector const statements = getStatementsToMove(context); - if (!interactiveRefactorArguments) { + if (!statements?.all?.length) return emptyArray; + + // Reject control-flow statements (always). + const s = statements.all[0]; + switch (s.kind) { + case SyntaxKind.IfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.WithStatement: + return emptyArray; + } + + // Block refactor when selection is inside an unbraced if/else statement + // This prevents extracting partial statements that would create invalid code + if (isInsideIfStatementWithoutBraces(context)) { return emptyArray; } - /** If the start/end nodes of the selection are inside a block like node do not show the `Move to file` code action - * This condition is used in order to show less often the `Move to file` code action */ - if (context.triggerReason === "implicit" && context.endPosition !== undefined) { - const startNodeAncestor = findAncestor(getTokenAtPosition(file, context.startPosition), isBlockLike); - const endNodeAncestor = findAncestor(getTokenAtPosition(file, context.endPosition), isBlockLike); - if (startNodeAncestor && !isSourceFile(startNodeAncestor) && endNodeAncestor && !isSourceFile(endNodeAncestor)) { - return emptyArray; - } + + if (!interactiveRefactorArguments) { + return emptyArray; } + + // Only offer move-to-file when the host/preferences allow creating/updating files. if (context.preferences.allowTextChangesInNewFiles && statements) { const affectedTextRange = { - start: { line: getLineAndCharacterOfPosition(file, statements.all[0].getStart(file)).line, offset: getLineAndCharacterOfPosition(file, statements.all[0].getStart(file)).character }, - end: { line: getLineAndCharacterOfPosition(file, last(statements.all).end).line, offset: getLineAndCharacterOfPosition(file, last(statements.all).end).character }, + start: { + line: getLineAndCharacterOfPosition( + file, + statements.all[0].getStart(file) + ).line, + offset: getLineAndCharacterOfPosition( + file, + statements.all[0].getStart(file) + ).character, + }, + end: { + line: getLineAndCharacterOfPosition( + file, + last(statements.all).end + ).line, + offset: getLineAndCharacterOfPosition( + file, + last(statements.all).end + ).character, + }, }; - return [{ name: refactorNameForMoveToFile, description, actions: [{ ...moveToFileAction, range: affectedTextRange }] }]; + return [ + { + name: refactorNameForMoveToFile, + description, + actions: [ + { ...moveToFileAction, range: affectedTextRange }, + ], + }, + ]; } + if (context.preferences.provideRefactorNotApplicableReason) { - return [{ name: refactorNameForMoveToFile, description, actions: [{ ...moveToFileAction, 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 getRefactorEditsToMoveToFile(context, actionName, interactiveRefactorArguments): RefactorEditInfo | undefined { - Debug.assert(actionName === refactorNameForMoveToFile, "Wrong refactor invoked"); + + getEditsForAction: function getRefactorEditsToMoveToFile( + context: RefactorContext, + actionName: string, + interactiveRefactorArguments + ): RefactorEditInfo | undefined { + Debug.assert( + actionName === refactorNameForMoveToFile, + "Wrong refactor invoked" + ); const statements = Debug.checkDefined(getStatementsToMove(context)); const { host, program } = context; - Debug.assert(interactiveRefactorArguments, "No interactive refactor arguments available"); + Debug.assert( + interactiveRefactorArguments, + "No interactive refactor arguments available" + ); const targetFile = interactiveRefactorArguments.targetFile; if (hasJSFileExtension(targetFile) || hasTSFileExtension(targetFile)) { - if (host.fileExists(targetFile) && program.getSourceFile(targetFile) === undefined) { - return error(getLocaleSpecificMessage(Diagnostics.Cannot_move_statements_to_the_selected_file)); + if ( + host.fileExists(targetFile) && + program.getSourceFile(targetFile) === undefined + ) { + return error( + getLocaleSpecificMessage( + Diagnostics.Cannot_move_statements_to_the_selected_file + ) + ); } - 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 }; + 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, + }; } - return error(getLocaleSpecificMessage(Diagnostics.Cannot_move_to_file_selected_file_is_invalid)); + return error( + getLocaleSpecificMessage( + Diagnostics.Cannot_move_to_file_selected_file_is_invalid + ) + ); }, }); -function error(notApplicableReason: string) { - return { edits: [], renameFilename: undefined, renameLocation: undefined, notApplicableReason }; +// ----------------------------- +// Helper implementations +// ----------------------------- + +function error(notApplicableReason: string): RefactorEditInfo { + return { + edits: [], + renameFilename: undefined, + renameLocation: undefined, + notApplicableReason, + }; +} + +/** + * Robust check: are start & end inside the same if statement's then/else clause which lacks braces? + */ +function isInsideIfStatementWithoutBraces(context: RefactorContext): boolean { + const { file, startPosition, endPosition } = context; + if (endPosition === undefined) return false; + + const startToken = getTokenAtPosition(file, startPosition); + const endToken = getTokenAtPosition(file, endPosition); + + const startIf = findAncestor(startToken, isIfStatement); + const endIf = findAncestor(endToken, isIfStatement); + + if (!startIf || startIf !== endIf) return false; + + const ifStmt = startIf; + + // THEN: check non-block thenStatement boundaries + const then = ifStmt.thenStatement; + if (then && !isBlock(then)) { + const thenStart = then.getStart(file); + const thenEnd = then.end; + if (startPosition >= thenStart && endPosition <= thenEnd) return true; + } + + // ELSE: only if plain statement (not else-if) and not a block + const el = ifStmt.elseStatement; + if (el && !isBlock(el) && !isIfStatement(el)) { + const elseStart = el.getStart(file); + const elseEnd = el.end; + if (startPosition >= elseStart && endPosition <= elseEnd) return true; + } + + return false; } -function doChange(context: RefactorContext, oldFile: SourceFile, targetFile: 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 { + // Final validation: ensure statements are not inside unbraced if/else + // This is a safety check in case getStatementsToMove was bypassed + if (isInsideIfStatementWithoutBraces(context)) { + return; // Silently abort - should have been caught earlier + } + const checker = program.getTypeChecker(); const isForNewFile = !host.fileExists(targetFile); const targetSourceFile = isForNewFile - ? createFutureSourceFile(targetFile, oldFile.externalModuleIndicator ? ModuleKind.ESNext : oldFile.commonJsModuleIndicator ? ModuleKind.CommonJS : undefined, program, host) + ? createFutureSourceFile( + targetFile, + oldFile.externalModuleIndicator + ? ModuleKind.ESNext + : oldFile.commonJsModuleIndicator + ? ModuleKind.CommonJS + : undefined, + program, + host + ) : Debug.checkDefined(program.getSourceFile(targetFile)); - const importAdderForOldFile = codefix.createImportAdder(oldFile, context.program, context.preferences, context.host); - const importAdderForNewFile = codefix.createImportAdder(targetSourceFile, context.program, context.preferences, context.host); - getNewStatementsAndRemoveFromOldFile(oldFile, targetSourceFile, getUsageInfo(oldFile, toMove.all, checker, isForNewFile ? undefined : getExistingLocals(targetSourceFile as SourceFile, toMove.all, checker)), changes, toMove, program, host, preferences, importAdderForNewFile, importAdderForOldFile); + const importAdderForOldFile = codefix.createImportAdder( + oldFile, + context.program, + context.preferences, + context.host + ); + const importAdderForNewFile = codefix.createImportAdder( + targetSourceFile, + context.program, + context.preferences, + context.host + ); + getNewStatementsAndRemoveFromOldFile( + oldFile, + targetSourceFile, + getUsageInfo( + oldFile, + toMove.all, + checker, + isForNewFile + ? undefined + : getExistingLocals( + targetSourceFile as SourceFile, + toMove.all, + checker + ) + ), + changes, + toMove, + program, + host, + preferences, + importAdderForNewFile, + importAdderForOldFile + ); if (isForNewFile) { - addNewFileToTsconfig(program, changes, oldFile.fileName, targetFile, hostGetCanonicalFileName(host)); + addNewFileToTsconfig( + program, + changes, + oldFile.fileName, + targetFile, + hostGetCanonicalFileName(host) + ); } } @@ -246,63 +457,154 @@ export function getNewStatementsAndRemoveFromOldFile( host: LanguageServiceHost, preferences: UserPreferences, importAdderForNewFile: codefix.ImportAdder, - importAdderForOldFile: codefix.ImportAdder, + importAdderForOldFile: codefix.ImportAdder ): void { const checker = program.getTypeChecker(); - const prologueDirectives = takeWhile(oldFile.statements, isPrologueDirective); + const prologueDirectives = takeWhile( + oldFile.statements, + isPrologueDirective + ); - const useEsModuleSyntax = !fileShouldUseJavaScriptRequire(targetFile.fileName, program, host, !!oldFile.commonJsModuleIndicator); + const useEsModuleSyntax = !fileShouldUseJavaScriptRequire( + targetFile.fileName, + program, + host, + !!oldFile.commonJsModuleIndicator + ); const quotePreference = getQuotePreference(oldFile, preferences); - addImportsForMovedSymbols(usage.oldFileImportsFromTargetFile, targetFile.fileName, importAdderForOldFile, program); - deleteUnusedOldImports(oldFile, toMove.all, usage.unusedImportsFromOldFile, importAdderForOldFile); + addImportsForMovedSymbols( + usage.oldFileImportsFromTargetFile, + targetFile.fileName, + importAdderForOldFile, + program + ); + deleteUnusedOldImports( + oldFile, + toMove.all, + usage.unusedImportsFromOldFile, + importAdderForOldFile + ); importAdderForOldFile.writeFixes(changes, quotePreference); deleteMovedStatements(oldFile, toMove.ranges, changes); - updateImportsInOtherFiles(changes, program, host, oldFile, usage.movedSymbols, targetFile.fileName, quotePreference); - addExportsInOldFile(oldFile, usage.targetFileImportsFromOldFile, changes, useEsModuleSyntax); - addTargetFileImports(oldFile, usage.oldImportsNeededByTargetFile, usage.targetFileImportsFromOldFile, checker, program, importAdderForNewFile); + updateImportsInOtherFiles( + changes, + program, + host, + oldFile, + usage.movedSymbols, + targetFile.fileName, + quotePreference + ); + addExportsInOldFile( + oldFile, + usage.targetFileImportsFromOldFile, + changes, + useEsModuleSyntax + ); + addTargetFileImports( + oldFile, + usage.oldImportsNeededByTargetFile, + usage.targetFileImportsFromOldFile, + checker, + program, + importAdderForNewFile + ); if (!isFullSourceFile(targetFile) && prologueDirectives.length) { // Ensure prologue directives come before imports - changes.insertStatementsInNewFile(targetFile.fileName, prologueDirectives, oldFile); + changes.insertStatementsInNewFile( + targetFile.fileName, + prologueDirectives, + oldFile + ); } importAdderForNewFile.writeFixes(changes, quotePreference); - const body = addExports(oldFile, toMove.all, arrayFrom(usage.oldFileImportsFromTargetFile.keys()), useEsModuleSyntax); + const body = addExports( + oldFile, + toMove.all, + arrayFrom(usage.oldFileImportsFromTargetFile.keys()), + useEsModuleSyntax + ); if (isFullSourceFile(targetFile) && targetFile.statements.length > 0) { moveStatementsToTargetFile(changes, program, body, targetFile, toMove); - } - else if (isFullSourceFile(targetFile)) { - changes.insertNodesAtEndOfFile(targetFile, body, /*blankLineBetween*/ false); - } - else { - changes.insertStatementsInNewFile(targetFile.fileName, importAdderForNewFile.hasFixes() ? [SyntaxKind.NewLineTrivia, ...body] : body, oldFile); + } else if (isFullSourceFile(targetFile)) { + changes.insertNodesAtEndOfFile( + targetFile, + body, + /*blankLineBetween*/ false + ); + } else { + changes.insertStatementsInNewFile( + targetFile.fileName, + importAdderForNewFile.hasFixes() + ? [SyntaxKind.NewLineTrivia, ...body] + : body, + oldFile + ); } } /** @internal */ -export 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; - const newFileAbsolutePath = normalizePath(combinePaths(oldFileName, "..", newFileNameWithExtension)); - const newFilePath = getRelativePathFromFile(cfg.fileName, newFileAbsolutePath, getCanonicalFileName); + 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"); + 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); + changes.insertNodeInListAfter( + cfg, + last(filesProp.initializer.elements), + factory.createStringLiteral(newFilePath), + filesProp.initializer.elements + ); } } -function deleteMovedStatements(sourceFile: SourceFile, moved: readonly StatementRange[], changes: textChanges.ChangeTracker) { +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[], toDelete: Set, importAdder: codefix.ImportAdder) { +function deleteUnusedOldImports( + oldFile: SourceFile, + toMove: readonly Statement[], + toDelete: Set, + importAdder: codefix.ImportAdder +) { for (const statement of oldFile.statements) { if (contains(toMove, statement)) continue; - forEachImportInStatement(statement, i => { - forEachAliasDeclarationInImportOrRequire(i, decl => { + forEachImportInStatement(statement, (i) => { + forEachAliasDeclarationInImportOrRequire(i, (decl) => { if (toDelete.has(decl.symbol)) { importAdder.removeExistingImport(decl); } @@ -312,7 +614,12 @@ function deleteUnusedOldImports(oldFile: SourceFile, toMove: readonly Statement[ } /** @internal */ -export function addExportsInOldFile(oldFile: SourceFile, targetFileImportsFromOldFile: Map, changes: textChanges.ChangeTracker, useEsModuleSyntax: boolean): void { +export function addExportsInOldFile( + oldFile: SourceFile, + targetFileImportsFromOldFile: Map, + changes: textChanges.ChangeTracker, + useEsModuleSyntax: boolean +): void { const markSeenTop = nodeSeenTracker(); // Needed because multiple declarations may appear in `const x = 0, y = 1;`. targetFileImportsFromOldFile.forEach((_, symbol) => { if (!symbol.declarations) { @@ -325,7 +632,13 @@ export function addExportsInOldFile(oldFile: SourceFile, targetFileImportsFromOl const top = getTopLevelDeclarationStatement(decl); if (markSeenTop(top)) { - addExportToChanges(oldFile, top, name, changes, useEsModuleSyntax); + addExportToChanges( + oldFile, + top, + name, + changes, + useEsModuleSyntax + ); } } }); @@ -338,35 +651,90 @@ function updateImportsInOtherFiles( oldFile: SourceFile, movedSymbols: Set, targetFileName: string, - quotePreference: QuotePreference, + 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; + 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); + ? getPropertySymbolFromBindingElement( + checker, + name.parent as ObjectBindingElementWithoutPropertyName + ) + : 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 pathToTargetFileWithExtension = resolvePath(getDirectoryPath(getNormalizedAbsolutePath(oldFile.fileName, program.getCurrentDirectory())), targetFileName); + deleteUnusedImports( + sourceFile, + importNode, + changes, + shouldMove + ); // These will be changed to imports from the new file + + const pathToTargetFileWithExtension = resolvePath( + getDirectoryPath( + getNormalizedAbsolutePath( + oldFile.fileName, + program.getCurrentDirectory() + ) + ), + targetFileName + ); // no self-imports - if (getStringComparer(!program.useCaseSensitiveFileNames())(pathToTargetFileWithExtension, sourceFile.fileName) === Comparison.EqualTo) return; - - const newModuleSpecifier = moduleSpecifiers.getModuleSpecifier(program.getCompilerOptions(), sourceFile, sourceFile.fileName, pathToTargetFileWithExtension, createModuleSpecifierResolutionHost(program, host)); - const newImportDeclaration = filterImport(importNode, makeStringLiteral(newModuleSpecifier, quotePreference), shouldMove); - if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration); + if ( + getStringComparer(!program.useCaseSensitiveFileNames())( + pathToTargetFileWithExtension, + sourceFile.fileName + ) === Comparison.EqualTo + ) + return; + + const newModuleSpecifier = moduleSpecifiers.getModuleSpecifier( + program.getCompilerOptions(), + sourceFile, + sourceFile.fileName, + pathToTargetFileWithExtension, + 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); + if (ns) + updateNamespaceLikeImport( + changes, + sourceFile, + checker, + movedSymbols, + newModuleSpecifier, + ns, + importNode, + quotePreference + ); }); } } @@ -375,14 +743,21 @@ function updateImportsInOtherFiles( 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; + 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}`); + return Debug.assertNever( + node, + `Unexpected node kind ${(node as SupportedImport).kind}` + ); } } @@ -394,98 +769,181 @@ function updateNamespaceLikeImport( newModuleSpecifier: string, oldImportId: Identifier, oldImportNode: SupportedImport, - quotePreference: QuotePreference, + quotePreference: QuotePreference ): void { - const preferredNewNamespaceName = moduleSpecifierToValidIdentifier(newModuleSpecifier, ScriptTarget.ESNext); + const preferredNewNamespaceName = 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); + 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; + const newNamespaceName = needUniqueName + ? getUniqueName(preferredNewNamespaceName, sourceFile) + : preferredNewNamespaceName; for (const ref of toChange) { - changes.replaceNode(sourceFile, ref, factory.createIdentifier(newNamespaceName)); + changes.replaceNode( + sourceFile, + ref, + factory.createIdentifier(newNamespaceName) + ); } - changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, preferredNewNamespaceName, newModuleSpecifier, quotePreference)); + changes.insertNodeAfter( + sourceFile, + oldImportNode, + updateNamespaceLikeImportNode( + oldImportNode, + preferredNewNamespaceName, + newModuleSpecifier, + quotePreference + ) + ); } } -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); + const newModuleString = makeStringLiteral( + newModuleSpecifier, + quotePreference + ); switch (node.kind) { case SyntaxKind.ImportDeclaration: return factory.createImportDeclaration( /*modifiers*/ undefined, - factory.createImportClause(/*phaseModifier*/ undefined, /*name*/ undefined, factory.createNamespaceImport(newNamespaceId)), + factory.createImportClause( + /*phaseModifier*/ undefined, + /*name*/ undefined, + factory.createNamespaceImport(newNamespaceId) + ), newModuleString, - /*attributes*/ undefined, + /*attributes*/ undefined ); case SyntaxKind.ImportEqualsDeclaration: - return factory.createImportEqualsDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, newNamespaceId, factory.createExternalModuleReference(newModuleString)); + return factory.createImportEqualsDeclaration( + /*modifiers*/ undefined, + /*isTypeOnly*/ false, + newNamespaceId, + factory.createExternalModuleReference(newModuleString) + ); case SyntaxKind.VariableDeclaration: - return factory.createVariableDeclaration(newNamespaceId, /*exclamationToken*/ undefined, /*type*/ undefined, createRequireCall(newModuleString)); + return factory.createVariableDeclaration( + newNamespaceId, + /*exclamationToken*/ undefined, + /*type*/ undefined, + createRequireCall(newModuleString) + ); default: - return Debug.assertNever(node, `Unexpected node kind ${(node as SupportedImport).kind}`); + 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]); + return factory.createCallExpression( + factory.createIdentifier("require"), + /*typeArguments*/ undefined, + [moduleSpecifier] + ); } function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike { - return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier - : i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression - : i.initializer.arguments[0]); + 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 { +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)) { + 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)) { + } else if (isVariableStatement(statement)) { for (const decl of statement.declarationList.declarations) { - if (decl.initializer && isRequireCall(decl.initializer, /*requireStringLiteralLikeArgument*/ true)) { + if ( + decl.initializer && + isRequireCall( + decl.initializer, + /*requireStringLiteralLikeArgument*/ true + ) + ) { cb(decl as SupportedImport); } } } } -function forEachAliasDeclarationInImportOrRequire(importOrRequire: SupportedImport, cb: (declaration: codefix.ImportOrRequireAliasDeclaration) => void): void { +function forEachAliasDeclarationInImportOrRequire( + importOrRequire: SupportedImport, + cb: (declaration: codefix.ImportOrRequireAliasDeclaration) => void +): void { if (importOrRequire.kind === SyntaxKind.ImportDeclaration) { if (importOrRequire.importClause?.name) { cb(importOrRequire.importClause); } - if (importOrRequire.importClause?.namedBindings?.kind === SyntaxKind.NamespaceImport) { + if ( + importOrRequire.importClause?.namedBindings?.kind === + SyntaxKind.NamespaceImport + ) { cb(importOrRequire.importClause.namedBindings); } - if (importOrRequire.importClause?.namedBindings?.kind === SyntaxKind.NamedImports) { - for (const element of importOrRequire.importClause.namedBindings.elements) { + if ( + importOrRequire.importClause?.namedBindings?.kind === + SyntaxKind.NamedImports + ) { + for (const element of importOrRequire.importClause.namedBindings + .elements) { cb(element); } } - } - else if (importOrRequire.kind === SyntaxKind.ImportEqualsDeclaration) { + } else if (importOrRequire.kind === SyntaxKind.ImportEqualsDeclaration) { cb(importOrRequire); - } - else if (importOrRequire.kind === SyntaxKind.VariableDeclaration) { + } else if (importOrRequire.kind === SyntaxKind.VariableDeclaration) { if (importOrRequire.name.kind === SyntaxKind.Identifier) { cb(importOrRequire); - } - else if (importOrRequire.name.kind === SyntaxKind.ObjectBindingPattern) { + } else if ( + importOrRequire.name.kind === SyntaxKind.ObjectBindingPattern + ) { for (const element of importOrRequire.name.elements) { if (isIdentifier(element.name)) { cb(element); @@ -497,9 +955,13 @@ function forEachAliasDeclarationInImportOrRequire(importOrRequire: SupportedImpo /** @internal */ export type SupportedImport = - | ImportDeclaration & { moduleSpecifier: StringLiteralLike; } - | ImportEqualsDeclaration & { moduleReference: ExternalModuleReference & { expression: StringLiteralLike; }; } - | VariableDeclaration & { initializer: RequireOrImportCall; }; + | (ImportDeclaration & { moduleSpecifier: StringLiteralLike }) + | (ImportEqualsDeclaration & { + moduleReference: ExternalModuleReference & { + expression: StringLiteralLike; + }; + }) + | (VariableDeclaration & { initializer: RequireOrImportCall }); /** @internal */ export type SupportedImportStatement = @@ -512,50 +974,124 @@ export function addImportsForMovedSymbols( symbols: Map, targetFileName: string, importAdder: codefix.ImportAdder, - program: Program, + program: Program ): void { for (const [symbol, isValidTypeOnlyUseSite] of symbols) { - const symbolName = getNameForExportedSymbol(symbol, getEmitScriptTarget(program.getCompilerOptions())); - const exportKind = symbol.name === "default" && symbol.parent ? ExportKind.Default : ExportKind.Named; - importAdder.addImportForNonExistentExport(symbolName, targetFileName, exportKind, symbol.flags, isValidTypeOnlyUseSite); + const symbolName = getNameForExportedSymbol( + symbol, + getEmitScriptTarget(program.getCompilerOptions()) + ); + const exportKind = + symbol.name === "default" && symbol.parent + ? ExportKind.Default + : ExportKind.Named; + importAdder.addImportForNonExistentExport( + symbolName, + targetFileName, + exportKind, + symbol.flags, + isValidTypeOnlyUseSite + ); } } -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 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 addExports(sourceFile: SourceFile, toMove: readonly Statement[], needExport: Symbol[], useEs6Exports: boolean): readonly Statement[] { - return flatMap(toMove, statement => { +function addExports( + sourceFile: SourceFile, + toMove: readonly Statement[], + needExport: Symbol[], + useEs6Exports: boolean +): readonly Statement[] { + return flatMap(toMove, (statement) => { if ( isTopLevelDeclarationStatement(statement) && !isExported(sourceFile, statement, useEs6Exports) && - forEachTopLevelDeclaration(statement, d => needExport.includes(Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol))) + forEachTopLevelDeclaration(statement, (d) => + needExport.includes( + Debug.checkDefined(tryCast(d, canHaveSymbol)?.symbol) + ) + ) ) { - const exports = addExport(getSynthesizedDeepClone(statement), useEs6Exports); + const exports = addExport( + getSynthesizedDeepClone(statement), + useEs6Exports + ); if (exports) return exports; } return getSynthesizedDeepClone(statement); }); } -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)); + 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))); + return ( + !!sourceFile.symbol && + !!sourceFile.symbol.exports && + getNamesToExportInCommonJS(decl).some((name) => + sourceFile.symbol.exports!.has(escapeLeadingUnderscores(name)) + ) + ); } -function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport, changes: textChanges.ChangeTracker, isUnused: (name: Identifier) => boolean): void { - if (importDecl.kind === SyntaxKind.ImportDeclaration && importDecl.importClause) { +function deleteUnusedImports( + sourceFile: SourceFile, + importDecl: SupportedImport, + changes: textChanges.ChangeTracker, + isUnused: (name: Identifier) => boolean +): void { + if ( + importDecl.kind === SyntaxKind.ImportDeclaration && + importDecl.importClause + ) { const { name, namedBindings } = importDecl.importClause; - if ((!name || isUnused(name)) && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports && namedBindings.elements.length !== 0 && namedBindings.elements.every(e => isUnused(e.name)))) { + if ( + (!name || isUnused(name)) && + (!namedBindings || + (namedBindings.kind === SyntaxKind.NamedImports && + namedBindings.elements.length !== 0 && + namedBindings.elements.every((e) => isUnused(e.name)))) + ) { return changes.delete(sourceFile, importDecl); } } - forEachAliasDeclarationInImportOrRequire(importDecl, i => { + forEachAliasDeclarationInImportOrRequire(importDecl, (i) => { if (i.name && isIdentifier(i.name) && isUnused(i.name)) { changes.delete(sourceFile, i); } @@ -563,66 +1099,151 @@ function deleteUnusedImports(sourceFile: SourceFile, importDecl: SupportedImport } /** @internal */ -export type TopLevelDeclarationStatement = NonVariableTopLevelDeclaration | VariableStatement; +export type TopLevelDeclarationStatement = + | NonVariableTopLevelDeclaration + | VariableStatement; -function isTopLevelDeclarationStatement(node: Node): node is TopLevelDeclarationStatement { - Debug.assert(isSourceFile(node.parent), "Node parent should be a SourceFile"); +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 { +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; +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); + 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); + 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); + return factory.updateVariableStatement( + d, + modifiers, + d.declarationList + ); case SyntaxKind.ModuleDeclaration: - return factory.updateModuleDeclaration(d, modifiers, d.name, d.body); + return factory.updateModuleDeclaration( + d, + modifiers, + d.name, + d.body + ); case SyntaxKind.EnumDeclaration: - return factory.updateEnumDeclaration(d, modifiers, d.name, d.members); + return factory.updateEnumDeclaration( + d, + modifiers, + d.name, + d.members + ); case SyntaxKind.TypeAliasDeclaration: - return factory.updateTypeAliasDeclaration(d, modifiers, d.name, d.typeParameters, d.type); + 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); + 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); + 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}`); + 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 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)), + factory.createPropertyAccessExpression( + factory.createIdentifier("exports"), + factory.createIdentifier(name) + ), SyntaxKind.EqualsToken, - factory.createIdentifier(name), - ), + factory.createIdentifier(name) + ) ); } -function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonly string[] { +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); + return mapDefined(decl.declarationList.declarations, (d) => + isIdentifier(d.name) ? d.name.text : undefined + ); case SyntaxKind.ModuleDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.TypeAliasDeclaration: @@ -633,43 +1254,81 @@ function getNamesToExportInCommonJS(decl: TopLevelDeclarationStatement): readonl // 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}`); + return Debug.assertNever( + decl, + `Unexpected decl kind ${ + (decl as TopLevelDeclarationStatement).kind + }` + ); } } -function filterImport(i: SupportedImport, moduleSpecifier: StringLiteralLike, keep: (name: Identifier) => boolean): SupportedImportStatement | undefined { +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); + 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.phaseModifier, defaultImport, namedBindings), getSynthesizedDeepClone(moduleSpecifier), /*attributes*/ undefined) + ? factory.createImportDeclaration( + /*modifiers*/ undefined, + factory.createImportClause( + clause.phaseModifier, + defaultImport, + namedBindings + ), + getSynthesizedDeepClone(moduleSpecifier), + /*attributes*/ 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; + return name + ? makeVariableStatement( + name, + i.type, + createRequireCall(moduleSpecifier), + i.parent.flags + ) + : undefined; } default: - return Debug.assertNever(i, `Unexpected import kind ${(i as SupportedImport).kind}`); + return Debug.assertNever( + i, + `Unexpected import kind ${(i as SupportedImport).kind}` + ); } } -function filterNamedBindings(namedBindings: NamedImportBindings, keep: (name: Identifier) => boolean): NamedImportBindings | undefined { +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; + } 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 { +function filterBindingName( + name: BindingName, + keep: (name: Identifier) => boolean +): BindingName | undefined { switch (name.kind) { case SyntaxKind.Identifier: return keep(name) ? name : undefined; @@ -677,37 +1336,65 @@ function filterBindingName(name: BindingName, keep: (name: Identifier) => boolea 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; + const newElements = name.elements.filter( + (prop) => + prop.propertyName || + !isIdentifier(prop.name) || + keep(prop.name) + ); + return newElements.length + ? factory.createObjectBindingPattern(newElements) + : undefined; } } } -function nameOfTopLevelDeclaration(d: TopLevelDeclaration): Identifier | undefined { - return isExpressionStatement(d) ? tryCast(d.expression.left.name, isIdentifier) : tryCast(d.name, isIdentifier); +function nameOfTopLevelDeclaration( + d: TopLevelDeclaration +): Identifier | undefined { + return isExpressionStatement(d) + ? tryCast(d.expression.left.name, isIdentifier) + : tryCast(d.name, isIdentifier); } -function getTopLevelDeclarationStatement(d: TopLevelDeclaration): TopLevelDeclarationStatement { +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)), + 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 { +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 { + if (!isExpressionStatement(decl)) + changes.insertExportModifier(sourceFile, decl); + } else { const names = getNamesToExportInCommonJS(decl); - if (names.length !== 0) changes.insertNodesAfter(sourceFile, decl, names.map(createExportAssignment)); + if (names.length !== 0) + changes.insertNodesAfter( + sourceFile, + decl, + names.map(createExportAssignment) + ); } } @@ -733,13 +1420,18 @@ export interface UsageInfo { /** Subset of movedSymbols that are still used elsewhere in the old file and must be imported back. */ readonly oldFileImportsFromTargetFile: Map; - readonly oldImportsNeededByTargetFile: Map; + readonly oldImportsNeededByTargetFile: Map< + Symbol, + [boolean, codefix.ImportOrRequireAliasDeclaration | undefined] + >; /** Subset of oldImportsNeededByTargetFile 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 = ...' +export type TopLevelExpressionStatement = ExpressionStatement & { + expression: BinaryExpression & { left: PropertyAccessExpression }; +}; // 'exports.x = ...' /** @internal */ export type NonVariableTopLevelDeclaration = @@ -754,33 +1446,45 @@ export type NonVariableTopLevelDeclaration = /** @internal */ export interface TopLevelVariableDeclaration extends VariableDeclaration { - parent: VariableDeclarationList & { parent: VariableStatement; }; + parent: VariableDeclarationList & { parent: VariableStatement }; } /** @internal */ -export type TopLevelDeclaration = NonVariableTopLevelDeclaration | TopLevelVariableDeclaration | BindingElement; +export type TopLevelDeclaration = + | NonVariableTopLevelDeclaration + | TopLevelVariableDeclaration + | BindingElement; /** @internal */ -export function createNewFileName(oldFile: SourceFile, program: Program, host: LanguageServiceHost, toMove: ToMove | undefined): string { +export function createNewFileName( + oldFile: SourceFile, + program: Program, + host: LanguageServiceHost, + toMove: ToMove | undefined +): string { const checker = program.getTypeChecker(); if (toMove) { 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.oldFileImportsFromTargetFile, usage.movedSymbols), - extension, + const newFileName = + combinePaths( + // new file is always placed in the same directory as the old file currentDirectory, - host, - ), - ) + // ensures the filename computed below isn't already taken + makeUniqueFilename( + // infers a name for the new file from the symbols being moved + inferNewFileName( + usage.oldFileImportsFromTargetFile, + usage.movedSymbols + ), + extension, + currentDirectory, + host + ) + ) + // new file has same extension as old file - + extension; + extension; return newFileName; } return ""; @@ -791,12 +1495,12 @@ interface RangeToMove { readonly afterLast: Statement | undefined; } -function getRangeToMove(context: RefactorContext): RangeToMove | undefined { +function _getRangeToMove(context: RefactorContext): RangeToMove | undefined { const { file } = context; const range = createTextRangeFromSpan(getRefactorContextSpan(context)); const { statements } = file; - let startNodeIndex = findIndex(statements, s => s.end > range.pos); + let startNodeIndex = findIndex(statements, (s) => s.end > range.pos); if (startNodeIndex === -1) return undefined; const startStatement = statements[startNodeIndex]; @@ -805,47 +1509,145 @@ function getRangeToMove(context: RefactorContext): RangeToMove | undefined { startNodeIndex = overloadRangeToMove.start; } - let endNodeIndex = findIndex(statements, s => s.end >= range.end, startNodeIndex); + let endNodeIndex = findIndex( + statements, + (s) => s.end >= range.end, + startNodeIndex + ); /** * [|const a = 2; * function foo() { * } * |] */ - if (endNodeIndex !== -1 && range.end <= statements[endNodeIndex].getStart()) { + if ( + endNodeIndex !== -1 && + range.end <= statements[endNodeIndex].getStart() + ) { endNodeIndex--; } - const endingOverloadRangeToMove = getOverloadRangeToMove(file, statements[endNodeIndex]); + const endingOverloadRangeToMove = getOverloadRangeToMove( + file, + statements[endNodeIndex] + ); if (endingOverloadRangeToMove) { endNodeIndex = endingOverloadRangeToMove.end; } return { - toMove: statements.slice(startNodeIndex, endNodeIndex === -1 ? statements.length : endNodeIndex + 1), - afterLast: endNodeIndex === -1 ? undefined : statements[endNodeIndex + 1], + toMove: statements.slice( + startNodeIndex, + endNodeIndex === -1 ? statements.length : endNodeIndex + 1 + ), + afterLast: + endNodeIndex === -1 ? undefined : statements[endNodeIndex + 1], }; } +function isTopLevelStatement( + statement: Statement, + sourceFile: SourceFile +): boolean { + return statement.parent === sourceFile; +} + /** @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 }; +export function getStatementsToMove( + context: RefactorContext +): ToMove | undefined { + const { file, startPosition, endPosition } = context; + if (endPosition === undefined) return undefined; + + const span = { start: startPosition, length: endPosition - startPosition }; + const selectedRange = { pos: span.start, end: span.start + span.length }; + + // 1. Find the smallest statement that fully contains the selection + const rootStatement = findAncestor( + getTokenAtPosition(file, startPosition), + (n) => isStatement(n) + ); + + if (!rootStatement) return undefined; + + // Reject non-top-level or nested statements directly (loop bodies, if-bodies etc.) + if (!isTopLevelStatement(rootStatement, file)) { + return undefined; + } + + // Reject statements inside unbraced if/else branches + // This prevents extracting partial statements like 'const x = 1;' from 'if (true) const x = 1;' + if (isInsideIfStatementWithoutBraces(context)) { + return undefined; + } + + // 2. Collect all statements in the file + const statements = file.statements; + + // 3. Filter statements that are fully contained in the selection + const selectedStatements = statements.filter((stmt) => + rangeContainsRange(selectedRange, stmt) + ); + + // 4. Reject empty / partial selections + if (selectedStatements.length === 0) return undefined; + + // 5. Reject selections that intersect but do not fully contain statements + for (const stmt of statements) { + const intersects = !( + stmt.end <= selectedRange.pos || stmt.pos >= selectedRange.end + ); + const fullyInside = + stmt.pos >= selectedRange.pos && stmt.end <= selectedRange.end; + + if (intersects && !fullyInside) { + return undefined; // partial = illegal + } + } + + // 6. Reject control-flow statements unconditionally + const first = selectedStatements[0].kind; + switch (first) { + case SyntaxKind.IfStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.WithStatement: + return undefined; + } + + const lastSelected = selectedStatements[selectedStatements.length - 1]; + const lastSelectedIndex = statements.indexOf(lastSelected); + const afterLast = + lastSelectedIndex >= 0 && lastSelectedIndex < statements.length - 1 + ? statements[lastSelectedIndex + 1] + : undefined; + + return { + all: selectedStatements, + ranges: [ + { + first: selectedStatements[0], + afterLast, + }, + ], + }; } /** @internal */ -export function containsJsx(statements: readonly Statement[] | undefined): Statement | undefined { - return find(statements, statement => !!(statement.transformFlags & TransformFlags.ContainsJsx)); +export function containsJsx( + statements: readonly Statement[] | undefined +): Statement | undefined { + return find( + statements, + (statement) => !!(statement.transformFlags & TransformFlags.ContainsJsx) + ); } -function isAllowedStatementToMove(statement: Statement): boolean { +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. @@ -859,52 +1661,126 @@ 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, /*requireStringLiteralLikeArgument*/ true)); + 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, existingTargetLocals: ReadonlySet = new Set(), enclosingRange?: TextRange): UsageInfo { +export function getUsageInfo( + oldFile: SourceFile, + toMove: readonly Statement[], + checker: TypeChecker, + existingTargetLocals: ReadonlySet = new Set(), + enclosingRange?: TextRange +): UsageInfo { const movedSymbols = new Set(); - const oldImportsNeededByTargetFile = new Map(); - const targetFileImportsFromOldFile = new Map(); + const oldImportsNeededByTargetFile = new Map< + Symbol, + [ + /*isValidTypeOnlyUseSite*/ boolean, + codefix.ImportOrRequireAliasDeclaration | undefined + ] + >(); + const targetFileImportsFromOldFile = new Map< + Symbol, + /*isValidTypeOnlyUseSite*/ boolean + >(); const jsxNamespaceSymbol = getJsxNamespaceSymbol(containsJsx(toMove)); - if (jsxNamespaceSymbol) { // Might not exist (e.g. in non-compiling code) - oldImportsNeededByTargetFile.set(jsxNamespaceSymbol, [false, tryCast(jsxNamespaceSymbol.declarations?.[0], (d): d is codefix.ImportOrRequireAliasDeclaration => isImportSpecifier(d) || isImportClause(d) || isNamespaceImport(d) || isImportEqualsDeclaration(d) || isBindingElement(d) || isVariableDeclaration(d))]); + if (jsxNamespaceSymbol) { + // Might not exist (e.g. in non-compiling code) + oldImportsNeededByTargetFile.set(jsxNamespaceSymbol, [ + false, + tryCast( + jsxNamespaceSymbol.declarations?.[0], + (d): d is codefix.ImportOrRequireAliasDeclaration => + isImportSpecifier(d) || + isImportClause(d) || + isNamespaceImport(d) || + isImportEqualsDeclaration(d) || + isBindingElement(d) || + isVariableDeclaration(d) + ), + ]); } 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")); + forEachTopLevelDeclaration(statement, (decl) => { + movedSymbols.add( + Debug.checkDefined( + isExpressionStatement(decl) + ? checker.getSymbolAtLocation(decl.expression.left) + : decl.symbol, + "Need a symbol here" + ) + ); }); } const unusedImportsFromOldFile = new Set(); for (const statement of toMove) { - forEachReference(statement, checker, enclosingRange, (symbol, isValidTypeOnlyUseSite) => { - if (!some(symbol.declarations)) { - return; - } - if (existingTargetLocals.has(skipAlias(symbol, checker))) { - unusedImportsFromOldFile.add(symbol); - return; - } - const importedDeclaration = find(symbol.declarations, isInImport); - if (importedDeclaration) { - const prevIsTypeOnly = oldImportsNeededByTargetFile.get(symbol); - oldImportsNeededByTargetFile.set(symbol, [ - prevIsTypeOnly === undefined ? isValidTypeOnlyUseSite : prevIsTypeOnly && isValidTypeOnlyUseSite, - tryCast(importedDeclaration, (d): d is codefix.ImportOrRequireAliasDeclaration => isImportSpecifier(d) || isImportClause(d) || isNamespaceImport(d) || isImportEqualsDeclaration(d) || isBindingElement(d) || isVariableDeclaration(d)), - ]); - } - else if (!movedSymbols.has(symbol) && every(symbol.declarations, decl => isTopLevelDeclaration(decl) && sourceFileOfTopLevelDeclaration(decl) === oldFile)) { - targetFileImportsFromOldFile.set(symbol, isValidTypeOnlyUseSite); + forEachReference( + statement, + checker, + enclosingRange, + (symbol, isValidTypeOnlyUseSite) => { + if (!some(symbol.declarations)) { + return; + } + if (existingTargetLocals.has(skipAlias(symbol, checker))) { + unusedImportsFromOldFile.add(symbol); + return; + } + const importedDeclaration = find( + symbol.declarations, + isInImport + ); + if (importedDeclaration) { + const prevIsTypeOnly = + oldImportsNeededByTargetFile.get(symbol); + oldImportsNeededByTargetFile.set(symbol, [ + prevIsTypeOnly === undefined + ? isValidTypeOnlyUseSite + : prevIsTypeOnly && isValidTypeOnlyUseSite, + tryCast( + importedDeclaration, + (d): d is codefix.ImportOrRequireAliasDeclaration => + isImportSpecifier(d) || + isImportClause(d) || + isNamespaceImport(d) || + isImportEqualsDeclaration(d) || + isBindingElement(d) || + isVariableDeclaration(d) + ), + ]); + } else if ( + !movedSymbols.has(symbol) && + every( + symbol.declarations, + (decl) => + isTopLevelDeclaration(decl) && + sourceFileOfTopLevelDeclaration(decl) === oldFile + ) + ) { + targetFileImportsFromOldFile.set( + symbol, + isValidTypeOnlyUseSite + ); + } } - }); + ); } for (const unusedImport of oldImportsNeededByTargetFile.keys()) { @@ -916,17 +1792,35 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], if (contains(toMove, statement)) continue; // jsxNamespaceSymbol will only be set iff it is in oldImportsNeededByTargetFile. - if (jsxNamespaceSymbol && !!(statement.transformFlags & TransformFlags.ContainsJsx)) { + if ( + jsxNamespaceSymbol && + !!(statement.transformFlags & TransformFlags.ContainsJsx) + ) { unusedImportsFromOldFile.delete(jsxNamespaceSymbol); } - forEachReference(statement, checker, enclosingRange, (symbol, isValidTypeOnlyUseSite) => { - if (movedSymbols.has(symbol)) oldFileImportsFromTargetFile.set(symbol, isValidTypeOnlyUseSite); - unusedImportsFromOldFile.delete(symbol); - }); + forEachReference( + statement, + checker, + enclosingRange, + (symbol, isValidTypeOnlyUseSite) => { + if (movedSymbols.has(symbol)) + oldFileImportsFromTargetFile.set( + symbol, + isValidTypeOnlyUseSite + ); + unusedImportsFromOldFile.delete(symbol); + } + ); } - return { movedSymbols, targetFileImportsFromOldFile, oldFileImportsFromTargetFile, oldImportsNeededByTargetFile, unusedImportsFromOldFile }; + return { + movedSymbols, + targetFileImportsFromOldFile, + oldFileImportsFromTargetFile, + oldImportsNeededByTargetFile, + unusedImportsFromOldFile, + }; function getJsxNamespaceSymbol(containsJsx: Node | undefined) { if (containsJsx === undefined) { @@ -938,28 +1832,51 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], // 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) + 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 { +function makeUniqueFilename( + proposedFilename: string, + extension: string, + inDirectory: string, + host: LanguageServiceHost +): string { let newFilename = proposedFilename; - for (let i = 1;; i++) { + for (let i = 1; ; i++) { const name = combinePaths(inDirectory, newFilename + extension); if (!host.fileExists(name)) return newFilename; newFilename = `${proposedFilename}.${i}`; } } -function inferNewFileName(importsFromNewFile: Map, movedSymbols: Set): string { - return forEachKey(importsFromNewFile, symbolNameNoDefault) || forEachKey(movedSymbols, symbolNameNoDefault) || "newFile"; +function inferNewFileName( + importsFromNewFile: Map, + movedSymbols: Set +): string { + return ( + forEachKey(importsFromNewFile, symbolNameNoDefault) || + forEachKey(movedSymbols, symbolNameNoDefault) || + "newFile" + ); } -function forEachReference(node: Node, checker: TypeChecker, enclosingRange: TextRange | undefined, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { +function forEachReference( + node: Node, + checker: TypeChecker, + enclosingRange: TextRange | undefined, + onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void +) { node.forEachChild(function cb(node) { if (isIdentifier(node) && !isDeclarationName(node)) { if (enclosingRange && !rangeContainsRange(enclosingRange, node)) { @@ -967,14 +1884,16 @@ function forEachReference(node: Node, checker: TypeChecker, enclosingRange: Text } const sym = checker.getSymbolAtLocation(node); if (sym) onReference(sym, isValidTypeOnlyAliasUseSite(node)); - } - else { + } else { node.forEachChild(cb); } }); } -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: @@ -983,14 +1902,28 @@ function forEachTopLevelDeclaration(statement: Statement, cb: (node: TopLevel case SyntaxKind.TypeAliasDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.ImportEqualsDeclaration: - return cb(statement as FunctionDeclaration | ClassDeclaration | EnumDeclaration | ModuleDeclaration | TypeAliasDeclaration | InterfaceDeclaration | 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)); + 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 + return isBinaryExpression(expression) && + getAssignmentDeclarationKind(expression) === + AssignmentDeclarationKind.ExportsProperty ? cb(statement as TopLevelExpressionStatement) : undefined; } @@ -1007,37 +1940,69 @@ export function isInImport(decl: Declaration): boolean { case SyntaxKind.VariableDeclaration: return isVariableDeclarationInImport(decl as VariableDeclaration); case SyntaxKind.BindingElement: - return isVariableDeclaration(decl.parent.parent) && isVariableDeclarationInImport(decl.parent.parent); + 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); + return ( + isSourceFile(decl.parent.parent.parent) && + !!decl.initializer && + isRequireCall( + decl.initializer, + /*requireStringLiteralLikeArgument*/ true + ) + ); } function isTopLevelDeclaration(node: Node): node is TopLevelDeclaration { - return isNonVariableTopLevelDeclaration(node) && isSourceFile(node.parent) || isVariableDeclaration(node) && isSourceFile(node.parent.parent.parent); + 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; + return isVariableDeclaration(node) + ? node.parent.parent.parent + : node.parent; } -function forEachTopLevelDeclarationInBindingName(name: BindingName, cb: (node: TopLevelDeclaration) => T): T | 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))); + 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)); + return firstDefined(name.elements, (em) => + isOmittedExpression(em) + ? undefined + : forEachTopLevelDeclarationInBindingName(em.name, cb) + ); default: - return Debug.assertNever(name, `Unexpected name kind ${(name as BindingName).kind}`); + return Debug.assertNever( + name, + `Unexpected name kind ${(name as BindingName).kind}` + ); } } -function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLevelDeclaration { +function isNonVariableTopLevelDeclaration( + node: Node +): node is NonVariableTopLevelDeclaration { switch (node.kind) { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: @@ -1052,31 +2017,76 @@ function isNonVariableTopLevelDeclaration(node: Node): node is NonVariableTopLev } } -function moveStatementsToTargetFile(changes: textChanges.ChangeTracker, program: Program, statements: readonly Statement[], targetFile: SourceFile, toMove: ToMove) { +function moveStatementsToTargetFile( + changes: textChanges.ChangeTracker, + program: Program, + statements: readonly Statement[], + targetFile: SourceFile, + toMove: ToMove +) { const removedExports = new Set(); const targetExports = targetFile.symbol?.exports; if (targetExports) { const checker = program.getTypeChecker(); - const targetToSourceExports = new Map>(); + const targetToSourceExports = new Map< + ExportDeclaration, + Set + >(); for (const node of toMove.all) { - if (isTopLevelDeclarationStatement(node) && hasSyntacticModifier(node, ModifierFlags.Export)) { - forEachTopLevelDeclaration(node, declaration => { - const targetDeclarations = canHaveSymbol(declaration) ? targetExports.get(declaration.symbol.escapedName)?.declarations : undefined; - const exportDeclaration = firstDefined(targetDeclarations, d => - isExportDeclaration(d) ? d : - isExportSpecifier(d) ? tryCast(d.parent.parent, isExportDeclaration) : undefined); - if (exportDeclaration && exportDeclaration.moduleSpecifier) { - targetToSourceExports.set(exportDeclaration, (targetToSourceExports.get(exportDeclaration) || new Set()).add(declaration)); + if ( + isTopLevelDeclarationStatement(node) && + hasSyntacticModifier(node, ModifierFlags.Export) + ) { + forEachTopLevelDeclaration(node, (declaration) => { + const targetDeclarations = canHaveSymbol(declaration) + ? targetExports.get(declaration.symbol.escapedName) + ?.declarations + : undefined; + const exportDeclaration = firstDefined( + targetDeclarations, + (d) => + isExportDeclaration(d) + ? d + : isExportSpecifier(d) + ? tryCast(d.parent.parent, isExportDeclaration) + : undefined + ); + if ( + exportDeclaration && + exportDeclaration.moduleSpecifier + ) { + targetToSourceExports.set( + exportDeclaration, + ( + targetToSourceExports.get(exportDeclaration) || + new Set() + ).add(declaration) + ); } }); } } - for (const [exportDeclaration, topLevelDeclarations] of arrayFrom(targetToSourceExports)) { - if (exportDeclaration.exportClause && isNamedExports(exportDeclaration.exportClause) && length(exportDeclaration.exportClause.elements)) { + for (const [exportDeclaration, topLevelDeclarations] of arrayFrom( + targetToSourceExports + )) { + if ( + exportDeclaration.exportClause && + isNamedExports(exportDeclaration.exportClause) && + length(exportDeclaration.exportClause.elements) + ) { const elements = exportDeclaration.exportClause.elements; - const updatedElements = filter(elements, elem => find(skipAlias(elem.symbol, checker).declarations, d => isTopLevelDeclaration(d) && topLevelDeclarations.has(d)) === undefined); + const updatedElements = filter( + elements, + (elem) => + find( + skipAlias(elem.symbol, checker).declarations, + (d) => + isTopLevelDeclaration(d) && + topLevelDeclarations.has(d) + ) === undefined + ); if (length(updatedElements) === 0) { changes.deleteNode(targetFile, exportDeclaration); @@ -1085,56 +2095,114 @@ function moveStatementsToTargetFile(changes: textChanges.ChangeTracker, program: } if (length(updatedElements) < length(elements)) { - changes.replaceNode(targetFile, exportDeclaration, factory.updateExportDeclaration(exportDeclaration, exportDeclaration.modifiers, exportDeclaration.isTypeOnly, factory.updateNamedExports(exportDeclaration.exportClause, factory.createNodeArray(updatedElements, elements.hasTrailingComma)), exportDeclaration.moduleSpecifier, exportDeclaration.attributes)); + changes.replaceNode( + targetFile, + exportDeclaration, + factory.updateExportDeclaration( + exportDeclaration, + exportDeclaration.modifiers, + exportDeclaration.isTypeOnly, + factory.updateNamedExports( + exportDeclaration.exportClause, + factory.createNodeArray( + updatedElements, + elements.hasTrailingComma + ) + ), + exportDeclaration.moduleSpecifier, + exportDeclaration.attributes + ) + ); } } } } - const lastReExport = findLast(targetFile.statements, n => isExportDeclaration(n) && !!n.moduleSpecifier && !removedExports.has(n)); + const lastReExport = findLast( + targetFile.statements, + (n) => + isExportDeclaration(n) && + !!n.moduleSpecifier && + !removedExports.has(n) + ); if (lastReExport) { - changes.insertNodesBefore(targetFile, lastReExport, statements, /*blankLineBetween*/ true); - } - else { - changes.insertNodesAfter(targetFile, targetFile.statements[targetFile.statements.length - 1], statements); + changes.insertNodesBefore( + targetFile, + lastReExport, + statements, + /*blankLineBetween*/ true + ); + } else { + changes.insertNodesAfter( + targetFile, + targetFile.statements[targetFile.statements.length - 1], + statements + ); } } function getOverloadRangeToMove(sourceFile: SourceFile, statement: Statement) { if (isFunctionLikeDeclaration(statement)) { const declarations = statement.symbol.declarations; - if (declarations === undefined || length(declarations) <= 1 || !contains(declarations, statement)) { + if ( + declarations === undefined || + length(declarations) <= 1 || + !contains(declarations, statement) + ) { return undefined; } const firstDecl = declarations[0]; const lastDecl = declarations[length(declarations) - 1]; - const statementsToMove = mapDefined(declarations, d => getSourceFileOfNode(d) === sourceFile && isStatement(d) ? d : undefined); - const end = findIndex(sourceFile.statements, s => s.end >= lastDecl.end); - const start = findIndex(sourceFile.statements, s => s.end >= firstDecl.end); + const statementsToMove = mapDefined(declarations, (d) => + getSourceFileOfNode(d) === sourceFile && isStatement(d) + ? d + : undefined + ); + const end = findIndex( + sourceFile.statements, + (s) => s.end >= lastDecl.end + ); + const start = findIndex( + sourceFile.statements, + (s) => s.end >= firstDecl.end + ); return { toMove: statementsToMove, start, end }; } return undefined; } /** @internal */ -export function getExistingLocals(sourceFile: SourceFile, statements: readonly Statement[], checker: TypeChecker): Set { +export function getExistingLocals( + sourceFile: SourceFile, + statements: readonly Statement[], + checker: TypeChecker +): Set { const existingLocals = new Set(); for (const moduleSpecifier of sourceFile.imports) { const declaration = importFromModuleSpecifier(moduleSpecifier); if ( - isImportDeclaration(declaration) && declaration.importClause && - declaration.importClause.namedBindings && isNamedImports(declaration.importClause.namedBindings) + isImportDeclaration(declaration) && + declaration.importClause && + declaration.importClause.namedBindings && + isNamedImports(declaration.importClause.namedBindings) ) { for (const e of declaration.importClause.namedBindings.elements) { - const symbol = checker.getSymbolAtLocation(e.propertyName || e.name); + const symbol = checker.getSymbolAtLocation( + e.propertyName || e.name + ); if (symbol) { existingLocals.add(skipAlias(symbol, checker)); } } } - if (isVariableDeclarationInitializedToRequire(declaration.parent) && isObjectBindingPattern(declaration.parent.name)) { + if ( + isVariableDeclarationInitializedToRequire(declaration.parent) && + isObjectBindingPattern(declaration.parent.name) + ) { for (const e of declaration.parent.name.elements) { - const symbol = checker.getSymbolAtLocation(e.propertyName || e.name); + const symbol = checker.getSymbolAtLocation( + e.propertyName || e.name + ); if (symbol) { existingLocals.add(skipAlias(symbol, checker)); } @@ -1143,12 +2211,21 @@ export function getExistingLocals(sourceFile: SourceFile, statements: readonly S } for (const statement of statements) { - forEachReference(statement, checker, /*enclosingRange*/ undefined, s => { - const symbol = skipAlias(s, checker); - if (symbol.valueDeclaration && getSourceFileOfNode(symbol.valueDeclaration).path === sourceFile.path) { - existingLocals.add(symbol); + forEachReference( + statement, + checker, + /*enclosingRange*/ undefined, + (s) => { + const symbol = skipAlias(s, checker); + if ( + symbol.valueDeclaration && + getSourceFileOfNode(symbol.valueDeclaration).path === + sourceFile.path + ) { + existingLocals.add(symbol); + } } - }); + ); } return existingLocals; } diff --git a/test-refactor.js b/test-refactor.js new file mode 100644 index 0000000000000..f34f355f13a7a --- /dev/null +++ b/test-refactor.js @@ -0,0 +1,53 @@ +// Quick test to understand the AST structure of bug.ts +const ts = require("./built/local/typescript.js"); +const fs = require("fs"); + +const code = fs.readFileSync("bug.ts", "utf8"); +const sourceFile = ts.createSourceFile( + "bug.ts", + code, + ts.ScriptTarget.Latest, + true +); + +console.log("=== AST Structure ==="); + +function printNode(node, indent = 0) { + const prefix = " ".repeat(indent); + console.log(`${prefix}${ts.SyntaxKind[node.kind]} (${node.pos}-${node.end})`); + if (ts.isIfStatement(node)) { + console.log( + `${prefix} thenStatement parent check: ${ + node.thenStatement.parent === node + }` + ); + console.log( + `${prefix} thenStatement.kind: ${ts.SyntaxKind[node.thenStatement.kind]}` + ); + } + node.forEachChild((child) => printNode(child, indent + 1)); +} + +printNode(sourceFile); + +// Now test with the position of "const x = 1" +console.log("\n=== Testing position 11 (start of 'const') ==="); +const token = ts.getTokenAtPosition(sourceFile, 11); +console.log( + `Token at pos 11: ${ts.SyntaxKind[token.kind]} (${token.pos}-${token.end})` +); + +let current = token; +while (current) { + console.log( + ` Ancestor: ${ts.SyntaxKind[current.kind]} (parent: ${ + current.parent ? ts.SyntaxKind[current.parent.kind] : "none" + })` + ); + if (ts.isStatement(current)) { + console.log(` ^^^ This is a statement!`); + console.log(` Parent is SourceFile? ${ts.isSourceFile(current.parent)}`); + break; + } + current = current.parent; +} diff --git a/tests/cases/compiler/typeAliasUsedAsValue.ts b/tests/cases/compiler/typeAliasUsedAsValue.ts new file mode 100644 index 0000000000000..70479fa55666a --- /dev/null +++ b/tests/cases/compiler/typeAliasUsedAsValue.ts @@ -0,0 +1,26 @@ +// @filename: mytypes.ts +export class MyClass { + public a: number; +} + +export type MyClassAlias = MyClass; + +// @filename: main.ts +import {MyClassAlias} from './mytypes'; + +let a: MyClassAlias = new MyClassAlias(); // Error: should show better message +let b = MyClassAlias; // Error: should show better message + +// Test with local type alias +type LocalAlias = string; +let c = LocalAlias; // Error: should show better message + +// Test with interface +interface MyInterface { + prop: string; +} +let d = MyInterface; // Error: should show better message + +// Test with generic type alias +type GenericAlias = T[]; +let e = GenericAlias; // Error: should show better message