Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ add_swift_syntax_library(SwiftRefactor
FormatRawStringLiteral.swift
IntegerLiteralUtilities.swift
MigrateToNewIfLetSyntax.swift
MoveMembersToExtension.swift
OpaqueParameterToGeneric.swift
RefactoringProvider.swift
RemoveSeparatorsFromIntegerLiteral.swift
Expand Down
101 changes: 101 additions & 0 deletions Sources/SwiftRefactor/MoveMembersToExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if compiler(>=6)
public import SwiftSyntax
#else
import SwiftSyntax
#endif

public struct MoveMembersToExtension: SyntaxRefactoringProvider {
public struct Context {
public let range: Range<AbsolutePosition>

public init(range: Range<AbsolutePosition>) {
self.range = range
}
}

public static func refactor(syntax: SourceFileSyntax, in context: Context) throws -> SourceFileSyntax {
guard
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 statementIndex = syntax.statements.index(of: statement)
else {
throw RefactoringNotApplicableError("Type declaration not found")
}

var selectedMembers = [MemberBlockItemSyntax]()
var selectedIdentifiers = [SyntaxIdentifier]()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: The preferred style in this repo is

Suggested change
var selectedMembers = [MemberBlockItemSyntax]()
var selectedIdentifiers = [SyntaxIdentifier]()
var selectedMembers: [MemberBlockItemSyntax] = []
var selectedIdentifiers: [SyntaxIdentifier] = []


var notMovedMembers: [MemberBlockItemSyntax] = []

declGroup.memberBlock.members.forEach {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use a for … in ... loop instead of forEach. I think forEach will also be diagnosed as an issue by swift format lint.

if context.range.overlaps($0.trimmedRange) {
if validateMember($0) {
selectedMembers.append($0)
selectedIdentifiers.append($0.id)
} else {
notMovedMembers.append($0)
}
}
}

guard !selectedMembers.isEmpty else {
throw RefactoringNotApplicableError("No members to move")
}

var updatedDeclGroup = declGroup
updatedDeclGroup.memberBlock.members = declGroup.memberBlock.members.filter { !selectedIdentifiers.contains($0.id) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can avoid selectedIdentifiers if you change this to

Suggested change
updatedDeclGroup.memberBlock.members = declGroup.memberBlock.members.filter { !selectedIdentifiers.contains($0.id) }
updatedDeclGroup.memberBlock.members = declGroup.memberBlock.members.filter { !selectedMembers.contains($0) }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this and found that it does not filter out unselected members.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That surprises me a lot because equality of syntax nodes is implemented by comparing their IDs. If you are sure that we need a selectedIdentifiers stick with it but I’d like to really understand why it’s necessary.

Copy link
Contributor Author

@myaumura myaumura Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 .filter { context.range.overlaps($0.trimmedRange) }  returns a new collection with new identifiers, and when comparing them, it does not remove the selected ones. Correct me if I’m wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, nice find. I totally forgot about that. If you convert the syntax node to an array first, we just hold on to element nodes and won’t perform a syntax tree transformation on filter, which should work around this. Ie. you should be able to do the following

    let selectedMembers = Array(declGroup.memberBlock.members).filter { context.range.overlaps($0.trimmedRange) }

If that doesn’t work, let’s stick with the selectedIdentifiers approach that you have.

let updatedItem = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let updatedItem = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup)))
let updatedStatement = statement.with(\.item, .decl(DeclSyntax(updatedDeclGroup)))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is still outstanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, missed this


let extensionMemberBlockSyntax = declGroup.memberBlock.with(\.members, MemberBlockItemListSyntax(selectedMembers))

var declName = decl.name
declName.trailingTrivia = declName.trailingTrivia.merging(.space)

let extensionDecl = ExtensionDeclSyntax(
leadingTrivia: .newlines(2),
extendedType: IdentifierTypeSyntax(
leadingTrivia: .space,
name: declName
),
memberBlock: extensionMemberBlockSyntax
)

var syntax = syntax
syntax.statements[statementIndex] = updatedItem
syntax.statements.insert(
CodeBlockItemSyntax(item: .decl(DeclSyntax(extensionDecl))),
at: syntax.statements.index(after: statementIndex)
)
return syntax
}

private static func validateMember(_ member: MemberBlockItemSyntax) -> Bool {

if member.decl.is(AccessorDeclSyntax.self) || member.decl.is(DeinitializerDeclSyntax.self)
|| member.decl.is(EnumCaseDeclSyntax.self)
{
return false
}

if let varDecl = member.decl.as(VariableDeclSyntax.self),
varDecl.bindings.contains(where: { $0.accessorBlock == nil || $0.initializer != nil })
{
return false
}

return true
}
}
Loading