Conversation
|
@ahoppen |
|
|
||
| public struct Context { | ||
| public let declName: TokenSyntax | ||
| public let selectedIdentifiers: [SyntaxIdentifier] |
There was a problem hiding this comment.
We should specify a range of declarations to move, not their identifiers. The base identifier names are not sufficient to identify a declaration if it’s overloaded.
| let tree = SourceFileSyntax.parse(from: &parser) | ||
| let context = makeContextFromClass(markers: markers, source: tree) | ||
| try assertRefactorConvert(tree, expected: expected, context: context) | ||
| } |
There was a problem hiding this comment.
We should make sure that we either disallow or correctly handle extracting members from nested types.
Suggested test cases:
struct Outer {
struct Inner {
func moveThis() {}
}
}struct Outer<T> {
struct Inner {
func moveThis() {}
}
}func outer() {
struct Inner {
func moveThis() {}
}
}There was a problem hiding this comment.
Added a couple of tests, the rest are in progress.
| var updatedStatements = syntax.statements | ||
| updatedStatements.remove(at: index) | ||
| updatedStatements.insert(updatedItem, at: index) | ||
| updatedStatements.append(CodeBlockItemSyntax(item: .decl(DeclSyntax(extensionDecl)))) |
There was a problem hiding this comment.
Should we insert the declaration after the updated item instead of at the end of the file?
ahoppen
left a comment
There was a problem hiding this comment.
I think the refactoring action almost works as I’d expect now, just a few more comments to give the implementation its final polish and make it easier to read / maintain in the future.
| #endif | ||
|
|
||
| public struct MoveMembersToExtension: SyntaxRefactoringProvider { | ||
|
|
There was a problem hiding this comment.
Superfluous newline, same in the functions below.
| throw RefactoringNotApplicableError("Type declaration not found") | ||
| } | ||
|
|
||
| let selectedMembers = declGroup.memberBlock.members.filter { context.range.contains($0.range) } |
There was a problem hiding this comment.
Should we check for intersects here so that you also move those declarations that you partially selected, eg. where you missed the function body?
Also, should we use $0.trimmedRange so that you don’t also move declarations whose leading whitespace or comments you may have selected?
| if member.decl.is(AccessorDeclSyntax.self) || member.decl.is(DeinitializerDeclSyntax.self) | ||
| || member.decl.is(EnumCaseDeclSyntax.self) | ||
| { | ||
| throw RefactoringNotApplicableError("Cannot move this type of declaration") |
There was a problem hiding this comment.
Should we be a little more specific in the error message about what we can’t move. I would imagine that this error message is annoying to get if you selected 200 lines of code and you don’t know what this is.
Actually, just thinking about it, when you select 5 functions and one deinitializer, maybe we should just move the 5 functions and leave the deinitializer where it is. And only emit an error like deinitializers cannot be moved to an extension if the deinitializer is the only declaration that was selected.
| try validateMember(member) | ||
| } | ||
|
|
||
| let remainingMembers = declGroup.memberBlock.members.filter { !context.range.contains($0.range) } |
There was a problem hiding this comment.
Instead of doing a range-based check here, should we check if selectedMembers contains that member? Something like the following.
| let remainingMembers = declGroup.memberBlock.members.filter { !context.range.contains($0.range) } | |
| let remainingMembers = declGroup.memberBlock.members.filter { !selectedMembers..contains($0) } |
|
|
||
| let updatedMemberBlock = declGroup.memberBlock.with(\.members, remainingMembers) | ||
| let updatedDeclGroup = declGroup.with(\.memberBlock, updatedMemberBlock) | ||
| let updatedItem = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup))) |
There was a problem hiding this comment.
| let updatedItem = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup))) | |
| let updatedStatement = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup))) |
| var declName = decl.name | ||
|
|
||
| // e.g, after `Outer<T>` trailing trivia is empty | ||
| if declName.trailingTrivia.isEmpty { | ||
| declName = declName.with(\.trailingTrivia, .space) | ||
| } |
There was a problem hiding this comment.
You can ensure that we have a space here by running
| var declName = decl.name | |
| // e.g, after `Outer<T>` trailing trivia is empty | |
| if declName.trailingTrivia.isEmpty { | |
| declName = declName.with(\.trailingTrivia, .space) | |
| } | |
| var declName = decl.name | |
| declName.trailingTrivia = declName.trailingTrivia.merging(.space) |
| updatedStatements.remove(at: index) | ||
| updatedStatements.insert(updatedItem, at: index) |
There was a problem hiding this comment.
Isn’t this the same as
| updatedStatements.remove(at: index) | |
| updatedStatements.insert(updatedItem, at: index) | |
| updatedStatements[index] = updatedItem |
| let statement = syntax.statements.first(where: { $0.item.range.contains(context.range) }), | ||
| let decl = statement.item.asProtocol(NamedDeclSyntax.self), | ||
| let declGroup = statement.item.asProtocol(DeclGroupSyntax.self), | ||
| let index = syntax.statements.index(of: statement) |
There was a problem hiding this comment.
Let’s be a little more specific which index this is.
| let index = syntax.statements.index(of: statement) | |
| let statementIndex = syntax.statements.index(of: statement) |
| var updatedStatements = syntax.statements | ||
| updatedStatements.remove(at: index) | ||
| updatedStatements.insert(updatedItem, at: index) | ||
| updatedStatements.insert( | ||
| CodeBlockItemSyntax(item: .decl(DeclSyntax(extensionDecl))), | ||
| at: syntax.statements.index(after: index) | ||
| ) | ||
| return syntax.with(\.statements, updatedStatements) |
There was a problem hiding this comment.
You can also modify syntax directly and don’t have to go through updatedStatements.
| var updatedStatements = syntax.statements | |
| updatedStatements.remove(at: index) | |
| updatedStatements.insert(updatedItem, at: index) | |
| updatedStatements.insert( | |
| CodeBlockItemSyntax(item: .decl(DeclSyntax(extensionDecl))), | |
| at: syntax.statements.index(after: index) | |
| ) | |
| return syntax.with(\.statements, updatedStatements) | |
| var syntax = syntax | |
| syntax.statements[index] = updatedItem | |
| syntax.statements.insert( | |
| CodeBlockItemSyntax(item: .decl(DeclSyntax(extensionDecl))), | |
| at: syntax.statements.index(after: index) | |
| ) | |
| return syntax |
| let context = MoveMembersToExtension.Context( | ||
| range: AbsolutePosition(utf8Offset: 11)..<AbsolutePosition(utf8Offset: 56) | ||
| ) |
There was a problem hiding this comment.
Instead of hard-coding UTF-8 offsets, which are hard to correlate back to the input text, could you use position markers (see extractMarkers). I find that usually that makes the tests significantly easier to read. Also, I think it would be good to add an assertion handler function so that the test reads as follows
assertMoveMembersToExtension(
"""
class Foo 1️⃣{
func foo() {
print("Hello world!")
}2️⃣
func bar() {
print("Hello world!")
}
}
""",
expected: """
class Foo {
func bar() {
print("Hello world!")
}
}
extension Foo {
func foo() {
print("Hello world!")
}
}
"""
)And I probably mis-placed the position markers here, please double check.
This PR implements the
MoveMembersToExtensionrefactoring action as requested in issue.