Skip to content

Commit 7ee1cf2

Browse files
committed
Code action to convert between computed property and stored property
1 parent fa81bf5 commit 7ee1cf2

5 files changed

+277
-0
lines changed

Sources/SwiftRefactor/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
add_swift_syntax_library(SwiftRefactor
1010
AddSeparatorsToIntegerLiteral.swift
1111
CallToTrailingClosures.swift
12+
ConvertComputedPropertyToStored.swift
13+
ConvertStoredPropertyToComputed.swift
1214
ExpandEditorPlaceholder.swift
1315
FormatRawStringLiteral.swift
1416
IntegerLiteralUtilities.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if swift(>=6)
14+
public import SwiftSyntax
15+
#else
16+
import SwiftSyntax
17+
#endif
18+
19+
public struct ConvertComputedPropertyToStored: SyntaxRefactoringProvider {
20+
public static func refactor(syntax: VariableDeclSyntax, in context: ()) -> VariableDeclSyntax? {
21+
guard syntax.bindings.count == 1, let binding = syntax.bindings.first,
22+
let accessorBlock = binding.accessorBlock, case let .getter(body) = accessorBlock.accessors, !body.isEmpty
23+
else {
24+
return nil
25+
}
26+
27+
var initializer: InitializerClauseSyntax?
28+
29+
if body.count > 1 {
30+
let closure = ClosureExprSyntax(
31+
statements: body.with(\.trailingTrivia, .newline)
32+
)
33+
34+
initializer = InitializerClauseSyntax(
35+
value: FunctionCallExprSyntax(
36+
leadingTrivia: .space,
37+
callee: closure
38+
)
39+
)
40+
} else if let item = body.first?.item.as(ExprSyntax.self) {
41+
initializer = InitializerClauseSyntax(
42+
value: item.with(\.leadingTrivia, .space).with(\.trailingTrivia, Trivia())
43+
)
44+
}
45+
46+
guard let initializer else { return nil }
47+
48+
let newBinding =
49+
binding
50+
.with(\.initializer, initializer)
51+
.with(\.accessorBlock, nil)
52+
53+
let bindingSpecifier = syntax.bindingSpecifier
54+
.with(\.tokenKind, .keyword(.let))
55+
56+
return
57+
syntax
58+
.with(\.bindingSpecifier, bindingSpecifier)
59+
.with(\.bindings, PatternBindingListSyntax([newBinding]))
60+
}
61+
}
62+
63+
fileprivate extension FunctionCallExprSyntax {
64+
65+
init(
66+
leadingTrivia: Trivia? = nil,
67+
callee: some ExprSyntaxProtocol,
68+
argumentList: () -> LabeledExprListSyntax = { [] },
69+
trailingTrivia: Trivia? = nil
70+
) {
71+
let argumentList = argumentList()
72+
self.init(
73+
leadingTrivia: leadingTrivia,
74+
calledExpression: callee,
75+
leftParen: .leftParenToken(),
76+
arguments: argumentList,
77+
rightParen: .rightParenToken(),
78+
trailingTrivia: trailingTrivia
79+
)
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if swift(>=6)
14+
public import SwiftSyntax
15+
#else
16+
import SwiftSyntax
17+
#endif
18+
19+
public struct ConvertStoredPropertyToComputed: SyntaxRefactoringProvider {
20+
public static func refactor(syntax: VariableDeclSyntax, in context: ()) -> VariableDeclSyntax? {
21+
guard syntax.bindings.count == 1, let binding = syntax.bindings.first, let initializer = binding.initializer else {
22+
return nil
23+
}
24+
25+
let body = CodeBlockItemSyntax(
26+
item: .expr(initializer.value)
27+
)
28+
29+
let newBinding =
30+
binding
31+
.with(\.initializer, nil)
32+
.with(
33+
\.accessorBlock,
34+
AccessorBlockSyntax(
35+
leftBrace: .leftBraceToken(trailingTrivia: .space),
36+
accessors: .getter(CodeBlockItemListSyntax([body])),
37+
rightBrace: .rightBraceToken(leadingTrivia: .space)
38+
)
39+
)
40+
41+
return
42+
syntax
43+
.with(\.bindingSpecifier, .keyword(.var, trailingTrivia: .space))
44+
.with(\.bindings, PatternBindingListSyntax([newBinding]))
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftRefactor
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
import XCTest
17+
import _SwiftSyntaxTestSupport
18+
19+
final class ConvertComputedPropertyToStoredTest: XCTestCase {
20+
func testToStored() throws {
21+
let baseline: DeclSyntax = """
22+
var defaultColor: Color { Color() }
23+
"""
24+
25+
let expected: DeclSyntax = """
26+
let defaultColor: Color = Color()
27+
"""
28+
29+
try assertRefactorConvert(baseline, expected: expected)
30+
}
31+
32+
func testToStoredWithMultipleStatementsInAccessor() throws {
33+
let baseline: DeclSyntax = """
34+
var defaultColor: Color {
35+
let color = Color()
36+
return color
37+
}
38+
"""
39+
40+
let expected: DeclSyntax = """
41+
let defaultColor: Color = {
42+
let color = Color()
43+
return color
44+
}()
45+
"""
46+
47+
try assertRefactorConvert(baseline, expected: expected)
48+
}
49+
}
50+
51+
fileprivate func assertRefactorConvert(
52+
_ callDecl: DeclSyntax,
53+
expected: DeclSyntax?,
54+
file: StaticString = #filePath,
55+
line: UInt = #line
56+
) throws {
57+
try assertRefactor(
58+
callDecl,
59+
context: (),
60+
provider: ConvertComputedPropertyToStored.self,
61+
expected: expected,
62+
file: file,
63+
line: line
64+
)
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftRefactor
14+
import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
import XCTest
17+
import _SwiftSyntaxTestSupport
18+
19+
final class ConvertStoredPropertyToComputedTest: XCTestCase {
20+
func testRefactoringStoredPropertyWithInitializer1() throws {
21+
let baseline: DeclSyntax = """
22+
static let defaultColor: Color = .red
23+
"""
24+
25+
let expected: DeclSyntax = """
26+
static var defaultColor: Color { .red }
27+
"""
28+
29+
try assertRefactorConvert(baseline, expected: expected)
30+
}
31+
32+
func testRefactoringStoredPropertyWithInitializer2() throws {
33+
let baseline: DeclSyntax = """
34+
static let defaultColor: Color = Color.red
35+
"""
36+
37+
let expected: DeclSyntax = """
38+
static var defaultColor: Color { Color.red }
39+
"""
40+
41+
try assertRefactorConvert(baseline, expected: expected)
42+
}
43+
44+
func testRefactoringStoredPropertyWithInitializer3() throws {
45+
let baseline: DeclSyntax = """
46+
var defaultColor: Color = Color.red
47+
"""
48+
49+
let expected: DeclSyntax = """
50+
var defaultColor: Color { Color.red }
51+
"""
52+
53+
try assertRefactorConvert(baseline, expected: expected)
54+
}
55+
56+
func testRefactoringStoredPropertyWithInitializer4() throws {
57+
let baseline: DeclSyntax = """
58+
var defaultColor: Color = Color()
59+
"""
60+
61+
let expected: DeclSyntax = """
62+
var defaultColor: Color { Color() }
63+
"""
64+
65+
try assertRefactorConvert(baseline, expected: expected)
66+
}
67+
}
68+
69+
fileprivate func assertRefactorConvert(
70+
_ callDecl: DeclSyntax,
71+
expected: DeclSyntax?,
72+
file: StaticString = #filePath,
73+
line: UInt = #line
74+
) throws {
75+
try assertRefactor(
76+
callDecl,
77+
context: (),
78+
provider: ConvertStoredPropertyToComputed.self,
79+
expected: expected,
80+
file: file,
81+
line: line
82+
)
83+
}

0 commit comments

Comments
 (0)