diff --git a/CHANGELOG.md b/CHANGELOG.md index 44552ec..37a42b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## dev branch / next version (2.x.x) +## 2.2.0 (2022-05-03) + +- added canRename API call + ## 2.1.4 (2022-05-02) - fixed handling of star imports diff --git a/haxelib.json b/haxelib.json index c5bfd4e..a5e109f 100644 --- a/haxelib.json +++ b/haxelib.json @@ -8,8 +8,8 @@ "refactor" ], "description": "A code renaming tool for Haxe", - "version": "2.1.4", - "releasenote": "fixed handling of star imports", + "version": "2.2.0", + "releasenote": "added canRename API call - see CHANGELOG", "contributors": [ "AlexHaxe" ], diff --git a/package-lock.json b/package-lock.json index c83f32f..000e0e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@haxecheckstyle/haxe-rename", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@haxecheckstyle/haxe-rename", - "version": "2.1.4", + "version": "2.2.0", "license": "MIT", "bin": { "haxe-rename": "bin/rename.js" diff --git a/package.json b/package.json index 285530f..cf9cb90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@haxecheckstyle/haxe-rename", - "version": "2.1.4", + "version": "2.2.0", "description": "Renaming tool for Haxe", "repository": { "type": "git", diff --git a/src/refactor/CanRefactorContext.hx b/src/refactor/CanRefactorContext.hx new file mode 100644 index 0000000..1fc1d0d --- /dev/null +++ b/src/refactor/CanRefactorContext.hx @@ -0,0 +1,16 @@ +package refactor; + +import refactor.ITyper; +import refactor.RefactorContext; +import refactor.discover.FileList; +import refactor.discover.NameMap; +import refactor.discover.TypeList; + +typedef CanRefactorContext = { + var nameMap:NameMap; + var fileList:FileList; + var typeList:TypeList; + var what:RefactorWhat; + var verboseLog:VerboseLogger; + var typer:Null; +} diff --git a/src/refactor/CanRefactorResult.hx b/src/refactor/CanRefactorResult.hx new file mode 100644 index 0000000..f31073a --- /dev/null +++ b/src/refactor/CanRefactorResult.hx @@ -0,0 +1,8 @@ +package refactor; + +import refactor.discover.IdentifierPos; + +typedef CanRefactorResult = { + var name:String; + var pos:IdentifierPos; +} diff --git a/src/refactor/Refactor.hx b/src/refactor/Refactor.hx index 2c523bc..9169771 100644 --- a/src/refactor/Refactor.hx +++ b/src/refactor/Refactor.hx @@ -2,6 +2,7 @@ package refactor; import refactor.discover.File; import refactor.discover.Identifier; +import refactor.discover.IdentifierPos; import refactor.rename.RenameAnonStructField; import refactor.rename.RenameEnumField; import refactor.rename.RenameField; @@ -12,6 +13,48 @@ import refactor.rename.RenameScopedLocal; import refactor.rename.RenameTypeName; class Refactor { + public static function canRename(context:CanRefactorContext):Promise { + var file:Null = context.fileList.getFile(context.what.fileName); + if (file == null) { + return Promise.reject(RefactorResult.NotFound.printRefactorResult()); + } + var identifier:Identifier = file.getIdentifier(context.what.pos); + if (identifier == null) { + return Promise.reject(RefactorResult.NotFound.printRefactorResult()); + } + return switch (identifier.type) { + case PackageName | ImportAlias | Abstract | Class | Enum | Interface | Typedef | ModuleLevelStaticVar | ModuleLevelStaticMethod | Property | + FieldVar(_) | Method(_) | TypedefField(_) | StructureField(_) | InterfaceProperty | InterfaceVar | InterfaceMethod | EnumField(_) | + ScopedLocal(_, _): + Promise.resolve({name: identifier.name, pos: identifier.pos}); + case ImportModul | UsingModul | Extends | Implements | AbstractOver | AbstractFrom | AbstractTo | TypeHint | StringConst | TypedParameter | + TypedefBase | Call(true) | CaseLabel(_): + Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + case Call(false) | Access | ArrayAccess(_) | ForIterator: + var candidate:Null = findActualWhat(context, file, identifier); + if (candidate == null) { + return Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + } + if (identifier.name.startsWith(candidate.name)) { + var pos:IdentifierPos = { + fileName: identifier.pos.fileName, + start: identifier.pos.start, + end: identifier.pos.start + context.what.toName.length + } + return Promise.resolve({name: candidate.name, pos: pos}); + } + if (identifier.name.endsWith(candidate.name)) { + var pos:IdentifierPos = { + fileName: identifier.pos.fileName, + start: identifier.pos.end - context.what.toName.length, + end: identifier.pos.end + } + return Promise.resolve({name: candidate.name, pos: pos}); + } + Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + } + } + public static function rename(context:RefactorContext):Promise { var file:Null = context.fileList.getFile(context.what.fileName); if (file == null) { @@ -68,7 +111,12 @@ class Refactor { Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); case Call(false) | Access | ArrayAccess(_) | ForIterator: context.verboseLog('rename "${identifier.name}" at call/access location - trying to find definition'); - findActualWhat(context, file, identifier); + var candidate:Null = findActualWhat(context, file, identifier); + if (candidate == null) { + return Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + } + context.what.pos = candidate.pos.start; + rename(context); case CaseLabel(_): Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); case ScopedLocal(scopeEnd, type): @@ -77,10 +125,10 @@ class Refactor { } } - static function findActualWhat(context:RefactorContext, file:File, identifier:Identifier):Promise { + static function findActualWhat(context:CanRefactorContext, file:File, identifier:Identifier):Null { var parts:Array = identifier.name.split("."); if (parts.length <= 0) { - return Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + return null; } var firstPart:String = parts.shift(); var onlyFields:Bool = false; @@ -92,7 +140,7 @@ class Refactor { } if (context.what.pos > identifier.pos.start + firstPart.length + offset) { // rename position is not in first part of dotted identifiier - return Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + return null; } var allUses:Array = file.findAllIdentifiers((i) -> i.name == firstPart); var candidate:Null = null; @@ -116,10 +164,6 @@ class Refactor { default: } } - if (candidate != null) { - context.what.pos = candidate.pos.start; - return rename(context); - } - return Promise.reject(RefactorResult.Unsupported(identifier.toString()).printRefactorResult()); + return candidate; } } diff --git a/src/refactor/RefactorContext.hx b/src/refactor/RefactorContext.hx index ddfb5e9..b312159 100644 --- a/src/refactor/RefactorContext.hx +++ b/src/refactor/RefactorContext.hx @@ -1,21 +1,12 @@ package refactor; import haxe.PosInfos; -import refactor.ITyper; -import refactor.discover.FileList; -import refactor.discover.NameMap; -import refactor.discover.TypeList; import refactor.edits.IEditableDocument; -typedef RefactorContext = { - var nameMap:NameMap; - var fileList:FileList; - var typeList:TypeList; +typedef RefactorContext = CanRefactorContext & { var what:RefactorWhat; var forRealExecute:Bool; var docFactory:(fileName:String) -> IEditableDocument; - var verboseLog:VerboseLogger; - var typer:Null; } typedef VerboseLogger = (text:String, ?pos:PosInfos) -> Void; diff --git a/src/refactor/discover/UsageCollector.hx b/src/refactor/discover/UsageCollector.hx index b92b874..f358be0 100644 --- a/src/refactor/discover/UsageCollector.hx +++ b/src/refactor/discover/UsageCollector.hx @@ -14,15 +14,8 @@ class UsageCollector { public function new() {} public function parseFile(content:ByteData, context:UsageContext) { - if (context.cache != null) { - var file:Null = context.cache.getFile(context.fileName, context.nameMap); - if (file != null) { - context.fileList.addFile(file); - for (type in file.typeList) { - context.typeList.addType(type); - } - return; - } + if (isCached(context)) { + return; } var root:Null = null; try { @@ -35,6 +28,21 @@ class UsageCollector { t = lexer.token(haxeparser.HaxeLexer.tok); } root = TokenTreeBuilder.buildTokenTree(tokens, content, TypeLevel); + parseFileWithTokens(root, context); + } catch (e:ParserError) { + throw 'failed to parse ${context.fileName} - ParserError: $e (${e.pos})'; + } catch (e:LexerError) { + throw 'failed to parse ${context.fileName} - LexerError: ${e.msg} (${e.pos})'; + } catch (e:Exception) { + throw 'failed to parse ${context.fileName} - ${e.details()}'; + } + } + + public function parseFileWithTokens(root:TokenTree, context:UsageContext) { + if (isCached(context)) { + return; + } + try { var file:File = new File(context.fileName); context.file = file; context.type = null; @@ -45,15 +53,26 @@ class UsageCollector { if (context.cache != null) { context.cache.storeFile(file); } - } catch (e:ParserError) { - throw 'failed to parse ${context.fileName} - ParserError: $e (${e.pos})'; - } catch (e:LexerError) { - throw 'failed to parse ${context.fileName} - LexerError: ${e.msg} (${e.pos})'; } catch (e:Exception) { throw 'failed to parse ${context.fileName} - ${e.details()}'; } } + function isCached(context:UsageContext):Bool { + if (context.cache != null) { + var file:Null = context.cache.getFile(context.fileName, context.nameMap); + if (file == null) { + return false; + } + context.fileList.addFile(file); + for (type in file.typeList) { + context.typeList.addType(type); + } + return true; + } + return false; + } + public function updateImportHx(context:UsageContext) { for (importHxFile in context.fileList.files) { var importHxPath:Path = new Path(importHxFile.name); diff --git a/test/refactor/TestBase.hx b/test/refactor/TestBase.hx index ea51b69..aa8604a 100644 --- a/test/refactor/TestBase.hx +++ b/test/refactor/TestBase.hx @@ -1,6 +1,5 @@ package refactor; -import haxe.CallStack; import haxe.Exception; import haxe.PosInfos; import js.lib.Promise; @@ -75,36 +74,47 @@ class TestBase implements ITest { function doRefactor(what:RefactorWhat, edits:Array, pos:PosInfos):Promise { var editList:TestEditList = new TestEditList(); - return Refactor.rename({ + return Refactor.canRename({ nameMap: usageContext.nameMap, fileList: usageContext.fileList, typeList: usageContext.typeList, what: what, - forRealExecute: true, - docFactory: (fileName) -> editList.newDoc(fileName), verboseLog: function(text:String, ?pos:PosInfos) { Sys.println('${pos.fileName}:${pos.lineNumber}: $text'); }, typer: null - }).then(function(success:RefactorResult) { - editList.sortEdits(); - Assert.equals(Done, success, pos); - Assert.equals(editList.docCounter, editList.docFinishedCounter, pos); - Assert.equals(edits.length, editList.edits.length, pos); - if (edits.length == editList.edits.length) { - for (index in 0...edits.length) { - var expected:TestEdit = edits[index]; - var actual:TestEdit = editList.edits[index]; - Assert.equals(expected.fileName, actual.fileName, expected.pos); - Assert.equals(fileEditToString(expected.edit), fileEditToString(actual.edit), expected.pos); + }).then(function(success:CanRefactorResult) { + return Refactor.rename({ + nameMap: usageContext.nameMap, + fileList: usageContext.fileList, + typeList: usageContext.typeList, + what: what, + forRealExecute: true, + docFactory: (fileName) -> editList.newDoc(fileName), + verboseLog: function(text:String, ?pos:PosInfos) { + Sys.println('${pos.fileName}:${pos.lineNumber}: $text'); + }, + typer: null + }).then(function(success:RefactorResult) { + editList.sortEdits(); + Assert.equals(Done, success, pos); + Assert.equals(editList.docCounter, editList.docFinishedCounter, pos); + Assert.equals(edits.length, editList.edits.length, pos); + if (edits.length == editList.edits.length) { + for (index in 0...edits.length) { + var expected:TestEdit = edits[index]; + var actual:TestEdit = editList.edits[index]; + Assert.equals(expected.fileName, actual.fileName, expected.pos); + Assert.equals(fileEditToString(expected.edit), fileEditToString(actual.edit), expected.pos); + } + } else { + for (edit in editList.edits) { + Sys.println(fileEditToString(edit.edit)); + } + Assert.fail("length mismatch - edits were not checked", pos); } - } else { - for (edit in editList.edits) { - Sys.println(fileEditToString(edit.edit)); - } - Assert.fail("length mismatch - edits were not checked", pos); - } - return Promise.resolve(success); + return Promise.resolve(success); + }); }); }