Skip to content

Commit

Permalink
Code action to convert between computed property and stored property
Browse files Browse the repository at this point in the history
  • Loading branch information
antigluten committed Jul 2, 2024
1 parent fa81bf5 commit 107417a
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Sources/SwiftRefactor/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
add_swift_syntax_library(SwiftRefactor
AddSeparatorsToIntegerLiteral.swift
CallToTrailingClosures.swift
ConvertComputedPropertyToStored.swift
ConvertStoredPropertyToComputed.swift
ExpandEditorPlaceholder.swift
FormatRawStringLiteral.swift
IntegerLiteralUtilities.swift
Expand Down
96 changes: 96 additions & 0 deletions Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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 swift(>=6)
public import SwiftSyntax
#else
import SwiftSyntax
#endif

public struct ConvertComputedPropertyToStored: SyntaxRefactoringProvider {
public static func refactor(syntax: VariableDeclSyntax, in context: ()) -> VariableDeclSyntax? {
guard syntax.bindings.count == 1, let binding = syntax.bindings.first,
let accessorBlock = binding.accessorBlock, case let .getter(body) = accessorBlock.accessors, !body.isEmpty
else {
return nil
}

let refactored = { (initializer: InitializerClauseSyntax) -> VariableDeclSyntax in
let newBinding =
binding
.with(\.initializer, initializer)
.with(\.accessorBlock, nil)

let bindingSpecifier = syntax.bindingSpecifier
.with(\.tokenKind, .keyword(.let))

return
syntax
.with(\.bindingSpecifier, bindingSpecifier)
.with(\.bindings, PatternBindingListSyntax([newBinding]))
}

guard body.count == 1 else {
let closure = ClosureExprSyntax(
leftBrace: accessorBlock.leftBrace,
statements: body,
rightBrace: accessorBlock.rightBrace
)

return refactored(
InitializerClauseSyntax(
equal: .equalToken(trailingTrivia: .space),
value: FunctionCallExprSyntax(callee: closure)
)
)
}

guard let item = body.first?.item else {
return nil
}

if let item = item.as(ReturnStmtSyntax.self), let expression = item.expression {
let trailingTrivia: Trivia = expression.leadingTrivia.isEmpty ? .space : []
let lineIndentation = syntax.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
return refactored(
InitializerClauseSyntax(
equal: .equalToken(trailingTrivia: trailingTrivia),
value: expression,
trailingTrivia: lineIndentation.merging(
accessorBlock.rightBrace.leadingTrivia.droppingTrailingWhitespace.droppingLeadingWhitespace
)
)
)
} else if var item = item.as(ExprSyntax.self) {
item.trailingTrivia = item.trailingTrivia.droppingTrailingWhitespace
return refactored(
InitializerClauseSyntax(
equal: .equalToken(trailingTrivia: .space),
value: item,
trailingTrivia: accessorBlock.trailingTrivia
)
)
}

return nil
}
}

fileprivate extension Trivia {
var droppingLeadingWhitespace: Trivia {
return Trivia(pieces: self.drop(while: \.isWhitespace))
}

var droppingTrailingWhitespace: Trivia {
return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed())
}
}
50 changes: 50 additions & 0 deletions Sources/SwiftRefactor/ConvertStoredPropertyToComputed.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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 swift(>=6)
public import SwiftSyntax
#else
import SwiftSyntax
#endif

public struct ConvertStoredPropertyToComputed: SyntaxRefactoringProvider {
public static func refactor(syntax: VariableDeclSyntax, in context: ()) -> VariableDeclSyntax? {
guard syntax.bindings.count == 1, let binding = syntax.bindings.first, let initializer = binding.initializer else {
return nil
}

let body = CodeBlockItemSyntax(
item: .expr(initializer.value)
)

let newBinding =
binding
.with(\.initializer, nil)
.with(
\.accessorBlock,
AccessorBlockSyntax(
leftBrace: .leftBraceToken(trailingTrivia: initializer.equal.trailingTrivia),
accessors: .getter(CodeBlockItemListSyntax([body])),
rightBrace: .rightBraceToken(leadingTrivia: .space)
)
)

let newBindingSpecifier =
syntax.bindingSpecifier
.with(\.tokenKind, .keyword(.var))

return
syntax
.with(\.bindingSpecifier, newBindingSpecifier)
.with(\.bindings, PatternBindingListSyntax([newBinding]))
}
}
163 changes: 163 additions & 0 deletions Tests/SwiftRefactorTest/ConvertComputedPropertyToStoredTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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
//
//===----------------------------------------------------------------------===//

import SwiftRefactor
import SwiftSyntax
import SwiftSyntaxBuilder
import XCTest
import _SwiftSyntaxTestSupport

final class ConvertComputedPropertyToStoredTest: XCTestCase {
func testToStored() throws {
let baseline: DeclSyntax = """
var defaultColor: Color { Color() /* some text */ }
"""

let expected: DeclSyntax = """
let defaultColor: Color = Color() /* some text */
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithReturnStatement() throws {
let baseline: DeclSyntax = """
var defaultColor: Color {
return Color()
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = Color()
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithReturnStatementAndTrailingComment() throws {
let baseline: DeclSyntax = """
var defaultColor: Color {
return Color() /* some text */
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = Color() /* some text */
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithReturnStatementAndTrailingCommentOnNewLine() throws {
let baseline: DeclSyntax = """
var defaultColor: Color {
return Color()
/* some text */
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = Color()
/* some text */
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithMultipleStatementsInAccessor() throws {
let baseline: DeclSyntax = """
var defaultColor: Color {
let color = Color()
return color
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = {
let color = Color()
return color
}()
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithMultipleStatementsInAccessorAndTrailingCommentsOnNewLine() throws {
let baseline: DeclSyntax = """
var defaultColor: Color {
let color = Color()
return color
// returns color
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = {
let color = Color()
return color
// returns color
}()
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoredWithMultipleStatementsInAccessorAndLeadingComments() throws {
let baseline: DeclSyntax = """
var defaultColor: Color { // returns color
let color = Color()
return color
}
"""

let expected: DeclSyntax = """
let defaultColor: Color = { // returns color
let color = Color()
return color
}()
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testToStoreWithSeparatingComments() throws {
let baseline: DeclSyntax = """
var x: Int {
return
/* One */ 1
}
"""

let expected: DeclSyntax = """
let x: Int =
/* One */ 1
"""

try assertRefactorConvert(baseline, expected: expected)
}
}

fileprivate func assertRefactorConvert(
_ callDecl: DeclSyntax,
expected: DeclSyntax?,
file: StaticString = #filePath,
line: UInt = #line
) throws {
try assertRefactor(
callDecl,
context: (),
provider: ConvertComputedPropertyToStored.self,
expected: expected,
file: file,
line: line
)
}
Loading

0 comments on commit 107417a

Please sign in to comment.