import { CancellationToken } from 'vscode-languageserver' ; import { getImportInfo } from '../analyzer/analyzerNodeInfo' ; import { AliasDeclaration , Declaration , isAliasDeclaration , isClassDeclaration , isFunctionDeclaration , isVariableDeclaration , } from '../analyzer/declaration' ; import { createSynthesizedAliasDeclaration , getNameFromDeclaration } from '../analyzer/declarationUtils' ; import { createImportedModuleDescriptor , ImportResolver , ModuleNameAndType } from '../analyzer/importResolver' ; import { getDirectoryLeadingDotsPointsTo , getImportGroupFromModuleNameAndType , getRelativeModuleName , getTextEditsForAutoImportInsertion , getTextEditsForAutoImportSymbolAddition , getTextRangeForImportNameDeletion , getTopLevelImports , ImportNameInfo , ImportStatement , ImportStatements , } from '../analyzer/importStatementUtils' ; import { getDottedNameWithGivenNodeAsLastName , getFirstAncestorOrSelfOfKind , getFullStatementRange , isFromImportAlias , isFromImportModuleName , isFromImportName , isImportAlias , isImportModuleName , isLastNameOfModuleName , } from '../analyzer/parseTreeUtils' ; import { ParseTreeWalker } from '../analyzer/parseTreeWalker' ; import { isStubFile } from '../analyzer/sourceMapper' ; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes' ; import { getOrAdd , removeArrayElements } from '../common/collectionUtils' ; import { ConfigOptions } from '../common/configOptions' ; import { isString } from '../common/core' ; import { assert , assertNever } from '../common/debug' ; import { FileEditAction } from '../common/editAction' ; import { FileSystem } from '../common/fileSystem' ; import { combinePaths , getDirectoryChangeKind , getDirectoryPath , getFileName , isDirectory , isFile , resolvePaths , stripFileExtension , } from '../common/pathUtils' ; import { convertOffsetToPosition , convertTextRangeToRange } from '../common/positionUtils' ; import { doRangesIntersect , extendRange , Range , rangesAreEqual , TextRange } from '../common/textRange' ; import { ImportAsNode , ImportFromAsNode , ImportFromNode , ImportNode , isExpressionNode , MemberAccessNode , ModuleNameNode , ModuleNode , NameNode , ParseNode , ParseNodeType , } from '../parser/parseNodes' ; import { ParseResults } from '../parser/parser' ; import { CollectionResult , DocumentSymbolCollector } from './documentSymbolCollector' ; enum UpdateType { File , Folder , Symbol } export class RenameModuleProvider { static createForModule ( importResolver , configOptions , evaluator , path , newPath , token ) { if ( ! importResolver . fileSystem . existsSync ( path ) ) { return undefined ; } if ( isFile ( importResolver . fileSystem , path ) ) { return this . _create ( importResolver , configOptions , evaluator , path , newPath , UpdateType . File , token ) ; } else if ( isDirectory ( importResolver . fileSystem , path ) ) { if ( getDirectoryChangeKind ( importResolver . fileSystem , path , newPath ) !== 'Renamed' ) { return undefined ; } let fileNameForPackage = combinePaths ( path , '__init__.pyi' ) ; if ( ! importResolver . fileSystem . existsSync ( fileNameForPackage ) ) { fileNameForPackage = combinePaths ( path , '__init__.py' ) ; if ( ! importResolver . fileSystem . existsSync ( fileNameForPackage ) ) { return undefined ; } } return this . _create ( importResolver , configOptions , evaluator , fileNameForPackage , combinePaths ( newPath , getFileName ( fileNameForPackage ) ) , UpdateType . Folder , token ) ; } return undefined ; } static createForSymbol ( importResolver , configOptions , evaluator , path , newPath , declarations , token ) { if ( ! importResolver . fileSystem . existsSync ( path ) ) { return undefined ; } const filteredDecls = declarations . filter ( ( d ) => isClassDeclaration ( d ) || isFunctionDeclaration ( d ) || isVariableDeclaration ( d ) ) ; if ( filteredDecls . length === 0 ) { return undefined ; } return this . _create ( importResolver , configOptions , evaluator , path , newPath , UpdateType . Symbol , filteredDecls , token ! ) ; } private static _create ( importResolver , configOptions , evaluator , moduleFilePath , newModuleFilePath , type , tokenOrDeclarations , token ? ) { const execEnv = configOptions . findExecEnvironment ( moduleFilePath ) ; const moduleName = importResolver . getModuleNameForImport ( moduleFilePath , execEnv ) ; if ( ! moduleName . moduleName ) { return undefined ; } const newModuleName = importResolver . getModuleNameForImport ( newModuleFilePath , execEnv ) ; if ( ! newModuleName . moduleName ) { return undefined ; } token = CancellationToken . is ( tokenOrDeclarations ) ? tokenOrDeclarations : token ; const declarations = CancellationToken . is ( tokenOrDeclarations ) ? [ ] : tokenOrDeclarations ; if ( declarations . length === 0 ) { declarations . push ( createSynthesizedAliasDeclaration ( moduleFilePath ) ) ; if ( isStubFile ( moduleFilePath ) ) { importResolver . resolveImport ( moduleFilePath , execEnv , createImportedModuleDescriptor ( moduleName . moduleName ) ) ; importResolver . getSourceFilesFromStub ( moduleFilePath , execEnv , false ) . forEach ( ( p ) => declarations ! . push ( createSynthesizedAliasDeclaration ( p ) ) ) ; } } return new RenameModuleProvider ( importResolver . fileSystem , evaluator , moduleFilePath , newModuleFilePath , moduleName , newModuleName , type , declarations , token ! ) ; } private readonly _newModuleFilePath : string ; private readonly _moduleNames : string [ ] ; private readonly _newModuleNames : string [ ] ; private readonly _onlyNameChanged : boolean ; private readonly _results = new Map < string , FileEditAction [ ] > ( ) ; private readonly _aliasIntroduced = new Set < ImportAsNode > ( ) ; private constructor ( private _fs , private _evaluator , private _moduleFilePath , newModuleFilePath , private _moduleNameAndType , private _newModuleNameAndType , private _type , private _declarations , private _token ) { this . _newModuleFilePath = resolvePaths ( newModuleFilePath ) ; this . _moduleNames = this . _moduleName . split ( '.' ) ; this . _newModuleNames = this . _newModuleName . split ( '.' ) ; if ( this . _moduleNames . length !== this . _newModuleNames . length ) { this . _onlyNameChanged = false ; return ; } let i = 0 ; for ( i = 0 ; i < this . _moduleNames . length - 1 ; i ++ ) { if ( this . _moduleNames [ i ] !== this . _newModuleNames [ i ] ) { break ; } } this . _onlyNameChanged = i === this . _moduleNames . length - 1 ; assert ( this . _type !== UpdateType . Folder || this . _onlyNameChanged , 'We only support simple rename for folder' ) ; } renameReferences ( filePath , parseResults ) { switch ( this . _type ) { case UpdateType . Folder : return this . _renameFolderReferences ( filePath , parseResults ) ; case UpdateType . File : return this . _renameModuleReferences ( filePath , parseResults ) ; case UpdateType . Symbol : return this . _updateSymbolReferences ( filePath , parseResults ) ; default : return assertNever ( this . _type , `${ this . _type } is unknown` ) ; } } private _updateSymbolReferences ( filePath , parseResults ) { const collector = new DocumentSymbolCollector ( getNameFromDeclaration ( this . _declarations [ 0 ] ) ?? '' , this . _declarations , this . _evaluator ! , this . _token , parseResults . parseTree , true ) ; const importStatements = getTopLevelImports ( parseResults . parseTree , true ) ; const imported = importStatements . orderedImports . find ( ( i ) => i . moduleName === this . _newModuleName ) ; const nameRemoved = new Set < number > ( ) ; const importUsed = new Map < ImportAsNode | ImportFromAsNode , MemberAccessNode [ ] > ( ) ; for ( const result of collector . collect ( ) ) { const nodeFound = result . node ; if ( nodeFound . nodeType === ParseNodeType . String ) { continue ; } if ( isFromImportName ( nodeFound ) ) { const fromNode = nodeFound . parent ?. parent as ImportFromNode ; const newModuleName = this . _getNewModuleName ( filePath , fromNode . module . leadingDots > 0 , false ) ; if ( fromNode . imports . length === 1 ) { this . _addResultWithTextRange ( filePath , fromNode . module , parseResults , newModuleName ) ; } else { const importFromAs = nodeFound . parent as ImportFromAsNode ; this . _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , fromNode . imports , importFromAs ) ; const importNameInfo = { name : importFromAs . name . value , alias : importFromAs . alias ?. value , } ; this . _addResultEdits ( this . _getTextEditsForNewOrExistingFromImport ( filePath , fromNode , parseResults , nameRemoved , importStatements , newModuleName , [ importNameInfo ] ) ) ; } continue ; } const dottedName = getDottedNameWithGivenNodeAsLastName ( nodeFound ) ; if ( dottedName === nodeFound || dottedName . nodeType !== ParseNodeType . MemberAccess ) { continue ; } const moduleName = dottedName . leftExpression . nodeType === ParseNodeType . MemberAccess ? dottedName . leftExpression . memberName : dottedName . leftExpression . nodeType === ParseNodeType . Name ? dottedName . leftExpression : undefined ; if ( ! moduleName ) { continue ; } const moduleDecl = this . _evaluator . getDeclarationsForNameNode ( moduleName ) ?. filter ( ( d ) => isAliasDeclaration ( d ) && ( d . node . nodeType === ParseNodeType . ImportAs || d . node . nodeType === ParseNodeType . ImportFromAs ) ) ; if ( ! moduleDecl || moduleDecl . length === 0 ) { continue ; } const importAs = moduleDecl [ 0 ] . node as ImportAsNode | ImportFromAsNode ; getOrAdd ( importUsed , importAs , ( ) => [ ] ) . push ( dottedName ) ; continue ; } for ( const [ key , value ] of importUsed ) { let referenceModuleName ; if ( this . _canReplaceImportName ( parseResults , key , value ) ) { const moduleName = this . _getReferenceModuleName ( importStatements , imported ) ; if ( key . nodeType === ParseNodeType . ImportAs ) { if ( moduleName ) { referenceModuleName = moduleName ; this . _addImportNameDeletion ( filePath , parseResults , nameRemoved , ( key . parent as ImportNode ) . list , key ) ; } else { referenceModuleName = key . alias ? key . alias . value : this . _newModuleName ; this . _addResultWithTextRange ( filePath , key . module , parseResults , this . _newModuleName ) ; } } else { if ( moduleName ) { referenceModuleName = moduleName ; this . _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , ( key . parent as ImportFromNode ) . imports , key ) ; } else { const fromNode = key . parent as ImportFromNode ; const newModuleName = this . _getNewModuleName ( filePath , fromNode . module . leadingDots > 0 , true ) ; referenceModuleName = key . alias ? key . alias . value : this . _newLastModuleName ; this . _addResultWithTextRange ( filePath , fromNode . module , parseResults , newModuleName ) ; this . _addResultWithTextRange ( filePath , key . name , parseResults , this . _newLastModuleName ) ; } } } else { const moduleName = this . _getReferenceModuleName ( importStatements , imported ) ; if ( moduleName ) { referenceModuleName = moduleName ; } else { referenceModuleName = this . _newModuleName ; this . _addResultEdits ( getTextEditsForAutoImportInsertion ( [ ] , importStatements , this . _newModuleName , getImportGroupFromModuleNameAndType ( this . _newModuleNameAndType ) , parseResults , convertOffsetToPosition ( parseResults . parseTree . length , parseResults . tokenizerOutput . lines ) ) . map ( ( e ) => ( { filePath , range : e . range , replacementText : e . replacementText } ) ) ) ; } } for ( const node of value ) { this . _addResultWithTextRange ( filePath , node . leftExpression , parseResults , referenceModuleName ) ; } } } private _getReferenceModuleName ( importStatements , imported ) { if ( imported && imported . node . nodeType === ParseNodeType . Import ) { return imported . subnode ?. alias ? imported . subnode . alias . value : this . _newModuleName ; } else if ( importStatements . implicitImports ?. has ( this . _newModuleFilePath ) ) { const fromImportAs = importStatements . implicitImports . get ( this . _newModuleFilePath ) ! ; return fromImportAs . alias ? fromImportAs . alias . value : fromImportAs . name . value ; } return undefined ; } private _canReplaceImportName ( parseResults , importAs , symbolReferences ) { const nameToBind = importAs . alias ?? ( importAs . nodeType === ParseNodeType . ImportAs ? importAs . module . nameParts [ importAs . module . nameParts . length - 1 ] : importAs . name ) ; const declarations = DocumentSymbolCollector . getDeclarationsForNode ( nameToBind , this . _evaluator , false , this . _token ) ; if ( declarations . length === 0 ) { return false ; } const collector = new DocumentSymbolCollector ( nameToBind . value , declarations , this . _evaluator ! , this . _token , parseResults . parseTree , true ) ; for ( const result of collector . collect ( ) ) { if ( isImportModuleName ( result . node ) || isImportAlias ( result . node ) || isFromImportModuleName ( result . node ) || isFromImportName ( result . node ) || isFromImportAlias ( result . node ) ) { continue ; } if ( ! symbolReferences . some ( ( s ) => TextRange . containsRange ( s , result . node ) ) ) { return false ; } } return true ; } private _renameFolderReferences ( filePath , parseResults ) { const collector = new DocumentSymbolCollector ( this . lastModuleName , this . _declarations , this . _evaluator ! , this . _token , parseResults . parseTree , true ) ; for ( const result of collector . collect ( ) ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; } } private _renameModuleReferences ( filePath , parseResults ) { const collector = new DocumentSymbolCollector ( this . lastModuleName , this . _declarations , this . _evaluator ! , this . _token , parseResults . parseTree , true ) ; const nameRemoved = new Set < number > ( ) ; const results = collector . collect ( ) ; this . _updateModuleReferences ( filePath , parseResults , nameRemoved , results ) ; this . _updateRelativeModuleNamePath ( filePath , parseResults , nameRemoved , results ) ; } private _updateRelativeModuleNamePath ( filePath , parseResults , nameRemoved , results ) { if ( filePath !== this . _moduleFilePath ) { return ; } let importStatements ; for ( const edit of this . _getNewRelativeModuleNamesForFileMoved ( filePath , ModuleNameCollector . collect ( parseResults . parseTree ) . filter ( ( m ) => ! results . some ( ( r ) => TextRange . containsRange ( m . parent ! , r . node ) ) ) ) ) { this . _addResultWithTextRange ( filePath , edit . moduleName , parseResults , edit . newModuleName ) ; if ( ! edit . itemsToMove ) { continue ; } const fromNode = edit . moduleName . parent as ImportFromNode ; for ( const importFromAs of edit . itemsToMove ) { this . _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , fromNode . imports , importFromAs ) ; } importStatements = importStatements ?? getTopLevelImports ( parseResults . parseTree , false ) ; this . _addResultEdits ( this . _getTextEditsForNewOrExistingFromImport ( filePath , fromNode , parseResults , nameRemoved , importStatements , getRelativeModuleName ( this . _fs , this . _newModuleFilePath , this . _newModuleFilePath , false , true ) , edit . itemsToMove . map ( ( i ) => { return { name : i . name . value , alias : i . alias ?. value } ; } ) ) ) ; } } private _updateModuleReferences ( filePath , parseResults , nameRemoved , results ) { let importStatements ; for ( const result of results ) { const nodeFound = result . node ; if ( nodeFound . nodeType === ParseNodeType . String ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } if ( isImportModuleName ( nodeFound ) ) { if ( ! isLastNameOfModuleName ( nodeFound ) ) { continue ; } const moduleNameNode = getFirstAncestorOrSelfOfKind ( nodeFound , ParseNodeType . ModuleName ) ! ; if ( moduleNameNode . nameParts . length === 1 && moduleNameNode . parent ?. nodeType === ParseNodeType . ImportAs && ! moduleNameNode . parent . alias && this . _newModuleNames . length > 1 ) { this . _aliasIntroduced . add ( moduleNameNode . parent ) ; this . _addResultWithTextRange ( filePath , moduleNameNode , parseResults , `${ this . _newModuleName } as ${ this . _newLastModuleName }` ) ; continue ; } this . _addResultWithTextRange ( filePath , moduleNameNode , parseResults , this . _newModuleName ) ; continue ; } if ( isImportAlias ( nodeFound ) ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } if ( isFromImportModuleName ( nodeFound ) ) { if ( ! isLastNameOfModuleName ( nodeFound ) ) { continue ; } const moduleNameNode = getFirstAncestorOrSelfOfKind ( nodeFound , ParseNodeType . ModuleName ) ! ; const fromNode = moduleNameNode . parent as ImportFromNode ; const exportedSymbols = [ ] ; const subModules = [ ] ; for ( const importFromAs of fromNode . imports ) { if ( this . _isExportedSymbol ( importFromAs . name ) ) { exportedSymbols . push ( importFromAs ) ; } else { subModules . push ( importFromAs ) ; } } if ( subModules . length === 0 ) { this . _addResultWithTextRange ( filePath , moduleNameNode , parseResults , this . _getNewModuleName ( filePath , moduleNameNode . leadingDots > 0 , false ) ) ; continue ; } if ( exportedSymbols . length === 0 ) { continue ; } if ( fromNode . module . leadingDots > 0 ) { for ( const edit of this . _getNewRelativeModuleNamesForFileMoved ( filePath , [ fromNode . module ] ) ) { this . _addResultWithTextRange ( filePath , edit . moduleName , parseResults , edit . newModuleName ) ; } } for ( const importFromAs of exportedSymbols ) { this . _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , fromNode . imports , importFromAs ) ; } importStatements = importStatements ?? getTopLevelImports ( parseResults . parseTree , false ) ; this . _addResultEdits ( this . _getTextEditsForNewOrExistingFromImport ( filePath , fromNode , parseResults , nameRemoved , importStatements , this . _newModuleName , exportedSymbols . map ( ( i ) => { const name = results . findIndex ( ( r ) => r . node === i . name ) >= 0 ? this . _newLastModuleName : i . name . value ; const alias = results . findIndex ( ( r ) => r . node === i . alias ) >= 0 ? this . _newLastModuleName : i . alias ?. value ; return { name , alias } ; } ) ) ) ; continue ; } if ( isFromImportName ( nodeFound ) ) { if ( nameRemoved . has ( nodeFound . id ) ) { continue ; } const fromNode = nodeFound . parent ?. parent as ImportFromNode ; const newModuleName = this . _getNewModuleName ( filePath , fromNode . module . leadingDots > 0 , true ) ; if ( this . _isExportedSymbol ( nodeFound ) ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } if ( fromNode . imports . length === 1 ) { this . _addResultWithTextRange ( filePath , fromNode . module , parseResults , newModuleName ) ; this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; } else { const importFromAs = nodeFound . parent as ImportFromAsNode ; if ( fromNode . module . leadingDots > 0 ) { for ( const edit of this . _getNewRelativeModuleNamesForFileMoved ( filePath , [ fromNode . module ] ) ) { this . _addResultWithTextRange ( filePath , edit . moduleName , parseResults , edit . newModuleName ) ; } } this . _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , fromNode . imports , importFromAs ) ; importStatements = importStatements ?? getTopLevelImports ( parseResults . parseTree , false ) ; const importNameInfo = { name : this . _newLastModuleName , alias : importFromAs . alias ?. value === this . lastModuleName ? this . _newLastModuleName : importFromAs . alias ?. value , } ; this . _addResultEdits ( this . _getTextEditsForNewOrExistingFromImport ( filePath , fromNode , parseResults , nameRemoved , importStatements , newModuleName , [ importNameInfo ] ) ) ; } continue ; } if ( isFromImportAlias ( nodeFound ) ) { if ( nameRemoved . has ( nodeFound . id ) ) { continue ; } this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } /** TODO: if we get more than 1 decls, flag it as attention needed */ const decls = DocumentSymbolCollector . getDeclarationsForNode ( nodeFound , this . _evaluator , false , this . _token ) . filter ( ( d ) => isAliasDeclaration ( d ) ) as AliasDeclaration [ ] ; if ( this . _onlyNameChanged ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } if ( decls ?. some ( ( d ) => ! d . usesLocalName && ( ! d . node || d . node . nodeType === ParseNodeType . ImportAs ) && ! this . _aliasIntroduced . has ( d . node ) ) ) { const dottedName = getDottedNameWithGivenNodeAsLastName ( nodeFound ) ; if ( dottedName . parent ?. nodeType !== ParseNodeType . MemberAccess ) { this . _addResultWithTextRange ( filePath , dottedName , parseResults , this . _newModuleName ) ; continue ; } const nextNameDecl = this . _evaluator . getDeclarationsForNameNode ( dottedName . parent . memberName ) ; if ( ! nextNameDecl || nextNameDecl . length === 0 ) { continue ; } this . _addResultWithTextRange ( filePath , dottedName , parseResults , this . _newModuleName ) ; continue ; } if ( result . node . value !== this . _newLastModuleName ) { this . _addResultWithTextRange ( filePath , result . range , parseResults , this . _newLastModuleName ) ; continue ; } } } private _getNewRelativeModuleNamesForFileMoved ( filePath , moduleNames ) { if ( filePath !== this . _moduleFilePath ) { return [ ] ; } const originalFileName = stripFileExtension ( getFileName ( filePath ) ) ; const originalInit = originalFileName === '__init__' ; const originalDirectory = getDirectoryPath ( filePath ) ; const newNames = [ ] ; for ( const moduleName of moduleNames ) { if ( moduleName . leadingDots === 0 ) { continue ; } const result = this . _getNewModuleNameInfoForFileMoved ( moduleName , originalInit , originalDirectory ) ; if ( ! result ) { continue ; } const newModuleName = getRelativeModuleName ( this . _fs , result . src , result . dest , false , true ) ; newNames . push ( { moduleName , newModuleName , itemsToMove : result . itemsToMove } ) ; } return newNames ; } private _getNewModuleNameInfoForFileMoved ( moduleName , originalInit , originalDirectory ) { const importInfo = getImportInfo ( moduleName ) ; if ( ! importInfo ) { return undefined ; } let importPath = importInfo . resolvedPaths [ importInfo . resolvedPaths . length - 1 ] ; if ( ! importPath ) { if ( moduleName . nameParts . length === 0 ) { const directory = getDirectoryLeadingDotsPointsTo ( originalDirectory , moduleName . leadingDots ) ; if ( ! directory ) { return undefined ; } importPath = combinePaths ( directory , '__init__.py' ) ; } else { return undefined ; } } if ( this . _moduleFilePath !== importPath || ! originalInit ) { return { src : this . _newModuleFilePath , dest : importPath } ; } const exportedSymbols = [ ] ; const subModules = [ ] ; for ( const importFromAs of ( moduleName . parent as ImportFromNode ) . imports ) { if ( this . _isExportedSymbol ( importFromAs . name ) ) { exportedSymbols . push ( importFromAs ) ; } else { subModules . push ( importFromAs ) ; } } if ( subModules . length === 0 ) { return { src : this . _newModuleFilePath , dest : this . _newModuleFilePath } ; } if ( exportedSymbols . length === 0 ) { return { src : this . _newModuleFilePath , dest : this . _moduleFilePath } ; } return { src : this . _newModuleFilePath , dest : this . _moduleFilePath , itemsToMove : [ ... exportedSymbols ] , } ; } private _isExportedSymbol ( nameNode ) { const decls = this . _evaluator . getDeclarationsForNameNode ( nameNode ) ; if ( ! decls ) { return false ; } return ! decls . some ( ( d ) => isAliasDeclaration ( d ) && d . submoduleFallback ) ; } private _getNewModuleName ( currentFilePath , isRelativePath , isLastPartImportName ) { const filePath = currentFilePath === this . _moduleFilePath ? this . _newModuleFilePath : currentFilePath ; const moduleName = isRelativePath ? getRelativeModuleName ( this . _fs , filePath , this . _newModuleFilePath , isLastPartImportName , true ) : this . _newModuleName ; if ( isLastPartImportName && moduleName . endsWith ( this . _newLastModuleName ) ) { const dotPrefix = moduleName === this . _newLastModuleName ? 0 : moduleName . length > this . _newLastModuleName . length + 1 ? moduleName [ moduleName . length - this . _newLastModuleName . length - 2 ] !== '.' ? 1 : 0 : 0 ; const length = moduleName . length - this . _newLastModuleName . length - dotPrefix ; const newModuleName = moduleName . substr ( 0 , length ) ; return newModuleName . length > 0 ? newModuleName : '.' ; } return moduleName ; } getEdits ( ) { const edits = [ ] ; this . _results . forEach ( ( v ) => edits . push ( ... v ) ) ; return edits ; } get lastModuleName ( ) { return this . _moduleNames [ this . _moduleNames . length - 1 ] ; } private get _moduleName ( ) { return this . _moduleNameAndType . moduleName ; } private get _newLastModuleName ( ) { return this . _newModuleNames [ this . _newModuleNames . length - 1 ] ; } private get _newModuleName ( ) { return this . _newModuleNameAndType . moduleName ; } private _addImportNameDeletion ( filePath , parseResults , nameRemoved , imports , importToDelete ) { this . _addImportNameDeletionInternal ( filePath , parseResults , nameRemoved , imports , importToDelete , ParseNodeType . Import ) ; nameRemoved . add ( importToDelete . module . id ) ; importToDelete . module . nameParts . forEach ( ( n ) => nameRemoved . add ( n . id ) ) ; if ( importToDelete . alias ) { nameRemoved . add ( importToDelete . alias . id ) ; } } private _addFromImportNameDeletion ( filePath , parseResults , nameRemoved , imports , importToDelete ) { this . _addImportNameDeletionInternal ( filePath , parseResults , nameRemoved , imports , importToDelete , ParseNodeType . ImportFrom ) ; nameRemoved . add ( importToDelete . name . id ) ; if ( importToDelete . alias ) { nameRemoved . add ( importToDelete . alias . id ) ; } } private _addImportNameDeletionInternal ( filePath , parseResults , nameRemoved , imports , importToDelete , importKind ) { const range = getTextRangeForImportNameDeletion ( imports , imports . findIndex ( ( v ) => v === importToDelete ) ) ; this . _addResultWithTextRange ( filePath , range , parseResults , '' ) ; nameRemoved . add ( importToDelete . id ) ; let lastImportIndexNotDeleted = 0 ; for ( lastImportIndexNotDeleted = imports . length - 1 ; lastImportIndexNotDeleted >= 0 ; lastImportIndexNotDeleted -- ) { if ( ! nameRemoved . has ( imports [ lastImportIndexNotDeleted ] . id ) ) { break ; } } if ( lastImportIndexNotDeleted === - 1 ) { const importStatement = getFirstAncestorOrSelfOfKind ( importToDelete , importKind ) ; if ( importStatement ) { this . _addResultWithRange ( filePath , getFullStatementRange ( importStatement , parseResults . tokenizerOutput ) , '' ) ; } } else if ( lastImportIndexNotDeleted >= 0 && lastImportIndexNotDeleted < imports . length - 2 ) { const start = TextRange . getEnd ( imports [ lastImportIndexNotDeleted ] ) ; const length = TextRange . getEnd ( imports [ lastImportIndexNotDeleted + 1 ] ) - start ; this . _addResultWithTextRange ( filePath , { start , length } , parseResults , '' ) ; } } private _addResultWithTextRange ( filePath , range , parseResults , newName ) { const existing = parseResults . text . substr ( range . start , range . length ) ; if ( existing === newName ) { return ; } this . _addResultWithRange ( filePath , convertTextRangeToRange ( range , parseResults . tokenizerOutput . lines ) , newName ) ; } private _addResultEdits ( edits ) { edits . forEach ( ( e ) => this . _addResultWithRange ( e . filePath , e . range , e . replacementText ) ) ; } private _getDeletionsForSpan ( filePathOrEdit , range ) { if ( isString ( filePathOrEdit ) ) { filePathOrEdit = this . _results . get ( filePathOrEdit ) ?? [ ] ; } return filePathOrEdit . filter ( ( e ) => e . replacementText === '' && doRangesIntersect ( e . range , range ) ) ; } private _removeEdits ( filePathOrEdit , edits ) { if ( isString ( filePathOrEdit ) ) { filePathOrEdit = this . _results . get ( filePathOrEdit ) ?? [ ] ; } removeArrayElements ( filePathOrEdit , ( f ) => edits . findIndex ( ( e ) => e === f ) >= 0 ) ; } private _addResultWithRange ( filePath , range , replacementText ) { const edits = getOrAdd ( this . _results , filePath , ( ) => [ ] ) ; if ( replacementText === '' ) { const deletions = this . _getDeletionsForSpan ( edits , range ) ; if ( deletions . length > 0 ) { this . _removeEdits ( edits , deletions ) ; extendRange ( range , deletions . map ( ( d ) => d . range ) ) ; } } if ( edits . some ( ( e ) => rangesAreEqual ( e . range , range ) && e . replacementText === replacementText ) ) { return ; } edits . push ( { filePath , range , replacementText } ) ; } private _getTextEditsForNewOrExistingFromImport ( filePath , currentFromImport , parseResults , nameRemoved , importStatements , moduleName , importNameInfo ) { const imported = importStatements . orderedImports . find ( ( i ) => i . moduleName === moduleName ) ; if ( imported && imported . node . nodeType === ParseNodeType . ImportFrom && ! imported . node . isWildcardImport ) { const edits = getTextEditsForAutoImportSymbolAddition ( importNameInfo , imported , parseResults ) ; if ( imported . node !== currentFromImport ) { return edits . map ( ( e ) => ( { filePath , range : e . range , replacementText : e . replacementText } ) ) ; } if ( this . _onlyNameChanged && importNameInfo . length === 1 && edits . length === 1 ) { const deletions = this . _getDeletionsForSpan ( filePath , edits [ 0 ] . range ) ; if ( deletions . length === 0 ) { return [ { filePath , range : edits [ 0 ] . range , replacementText : edits [ 0 ] . replacementText } ] ; } else { const alias = importNameInfo [ 0 ] . alias === this . _newLastModuleName ? this . lastModuleName : importNameInfo [ 0 ] . alias ; const importName = currentFromImport . imports . find ( ( i ) => i . name . value === this . lastModuleName && i . alias ?. value === alias ) ; if ( importName ) { this . _removeEdits ( filePath , deletions ) ; if ( importName . alias ) { nameRemoved . delete ( importName . alias . id ) ; } return [ { filePath , range : convertTextRangeToRange ( importName . name , parseResults . tokenizerOutput . lines ) , replacementText : this . _newLastModuleName , } , ] ; } } } } return getTextEditsForAutoImportInsertion ( importNameInfo , importStatements , moduleName , getImportGroupFromModuleNameAndType ( this . _newModuleNameAndType ) , parseResults , convertOffsetToPosition ( parseResults . parseTree . length , parseResults . tokenizerOutput . lines ) ) . map ( ( e ) => ( { filePath , range : e . range , replacementText : e . replacementText } ) ) ; } } class ModuleNameCollector extends ParseTreeWalker { private readonly _result : ModuleNameNode [ ] = [ ] ; override walk ( node ) { if ( isExpressionNode ( node ) ) { return ; } super . walk ( node ) ; } override visitModuleName ( node ) { this . _result . push ( node ) ; return false ; } public static collect ( root ) { const collector = new ModuleNameCollector ( ) ; collector . walk ( root ) ; return collector . _result ; } }