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 1, 2024
1 parent fa81bf5 commit 7ee1cf2
Show file tree
Hide file tree
Showing 5 changed files with 277 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
81 changes: 81 additions & 0 deletions Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//===----------------------------------------------------------------------===//
//
// 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
}

var initializer: InitializerClauseSyntax?

if body.count > 1 {
let closure = ClosureExprSyntax(
statements: body.with(\.trailingTrivia, .newline)
)

initializer = InitializerClauseSyntax(
value: FunctionCallExprSyntax(
leadingTrivia: .space,
callee: closure
)
)
} else if let item = body.first?.item.as(ExprSyntax.self) {
initializer = InitializerClauseSyntax(
value: item.with(\.leadingTrivia, .space).with(\.trailingTrivia, Trivia())
)
}

guard let initializer else { return nil }

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]))
}
}

fileprivate extension FunctionCallExprSyntax {

init(
leadingTrivia: Trivia? = nil,
callee: some ExprSyntaxProtocol,
argumentList: () -> LabeledExprListSyntax = { [] },
trailingTrivia: Trivia? = nil
) {
let argumentList = argumentList()
self.init(
leadingTrivia: leadingTrivia,
calledExpression: callee,
leftParen: .leftParenToken(),
arguments: argumentList,
rightParen: .rightParenToken(),
trailingTrivia: trailingTrivia
)
}
}
46 changes: 46 additions & 0 deletions Sources/SwiftRefactor/ConvertStoredPropertyToComputed.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// 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: .space),
accessors: .getter(CodeBlockItemListSyntax([body])),
rightBrace: .rightBraceToken(leadingTrivia: .space)
)
)

return
syntax
.with(\.bindingSpecifier, .keyword(.var, trailingTrivia: .space))
.with(\.bindings, PatternBindingListSyntax([newBinding]))
}
}
65 changes: 65 additions & 0 deletions Tests/SwiftRefactorTest/ConvertComputedPropertyToStoredTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//===----------------------------------------------------------------------===//
//
// 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() }
"""

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

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)
}
}

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
)
}
83 changes: 83 additions & 0 deletions Tests/SwiftRefactorTest/ConvertStoredPropertyToComputedTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//===----------------------------------------------------------------------===//
//
// 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 ConvertStoredPropertyToComputedTest: XCTestCase {
func testRefactoringStoredPropertyWithInitializer1() throws {
let baseline: DeclSyntax = """
static let defaultColor: Color = .red
"""

let expected: DeclSyntax = """
static var defaultColor: Color { .red }
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testRefactoringStoredPropertyWithInitializer2() throws {
let baseline: DeclSyntax = """
static let defaultColor: Color = Color.red
"""

let expected: DeclSyntax = """
static var defaultColor: Color { Color.red }
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testRefactoringStoredPropertyWithInitializer3() throws {
let baseline: DeclSyntax = """
var defaultColor: Color = Color.red
"""

let expected: DeclSyntax = """
var defaultColor: Color { Color.red }
"""

try assertRefactorConvert(baseline, expected: expected)
}

func testRefactoringStoredPropertyWithInitializer4() throws {
let baseline: DeclSyntax = """
var defaultColor: Color = Color()
"""

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

try assertRefactorConvert(baseline, expected: expected)
}
}

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

0 comments on commit 7ee1cf2

Please sign in to comment.