Skip to content

Commit ebf4299

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

5 files changed

+494
-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,97 @@
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+
let refactored = { (initializer: InitializerClauseSyntax) -> VariableDeclSyntax in
28+
let newBinding =
29+
binding
30+
.with(\.initializer, initializer)
31+
.with(\.accessorBlock, nil)
32+
33+
let bindingSpecifier = syntax.bindingSpecifier
34+
.with(\.tokenKind, .keyword(.let))
35+
36+
return
37+
syntax
38+
.with(\.bindingSpecifier, bindingSpecifier)
39+
.with(\.bindings, PatternBindingListSyntax([newBinding]))
40+
}
41+
42+
guard body.count == 1 else {
43+
let closure = ClosureExprSyntax(
44+
leftBrace: accessorBlock.leftBrace,
45+
statements: body,
46+
rightBrace: accessorBlock.rightBrace
47+
)
48+
49+
return refactored(
50+
InitializerClauseSyntax(
51+
equal: .equalToken(trailingTrivia: .space),
52+
value: FunctionCallExprSyntax(callee: closure)
53+
)
54+
)
55+
}
56+
57+
guard let item = body.first?.item else {
58+
return nil
59+
}
60+
61+
if let item = item.as(ReturnStmtSyntax.self), let expression = item.expression {
62+
let trailingTrivia: Trivia = expression.leadingTrivia.isEmpty ? .space : []
63+
let lineIndentation = syntax.firstToken(viewMode: .sourceAccurate)?.indentationOfLine ?? []
64+
return refactored(
65+
InitializerClauseSyntax(
66+
leadingTrivia: accessorBlock.leftBrace.trivia,
67+
equal: .equalToken(trailingTrivia: trailingTrivia),
68+
value: expression,
69+
trailingTrivia: lineIndentation + accessorBlock.rightBrace.trivia.droppingTrailingWhitespace
70+
)
71+
)
72+
} else if var item = item.as(ExprSyntax.self) {
73+
item.trailingTrivia = item.trailingTrivia.droppingTrailingWhitespace
74+
return refactored(
75+
InitializerClauseSyntax(
76+
equal: .equalToken(trailingTrivia: .space),
77+
value: item,
78+
trailingTrivia: accessorBlock.trailingTrivia
79+
)
80+
)
81+
}
82+
83+
return nil
84+
}
85+
}
86+
87+
fileprivate extension TokenSyntax {
88+
var trivia: Trivia {
89+
return leadingTrivia + trailingTrivia
90+
}
91+
}
92+
93+
fileprivate extension Trivia {
94+
var droppingTrailingWhitespace: Trivia {
95+
return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed())
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 codeBlockSyntax: CodeBlockItemListSyntax
26+
27+
if let functionExpression = initializer.value.as(FunctionCallExprSyntax.self),
28+
let closureExpression = functionExpression.calledExpression.as(ClosureExprSyntax.self)
29+
{
30+
let statements = closureExpression.statements
31+
codeBlockSyntax =
32+
statements
33+
.with(
34+
\.leadingTrivia,
35+
functionExpression.leadingTrivia + closureExpression.leftBrace.leadingTrivia
36+
+ closureExpression.leftBrace.trailingTrivia + statements.leadingTrivia
37+
)
38+
.with(
39+
\.trailingTrivia,
40+
statements.trailingTrivia + closureExpression.trailingTrivia + closureExpression.rightBrace.leadingTrivia
41+
+ closureExpression.rightBrace.trailingTrivia + functionExpression.trailingTrivia
42+
)
43+
} else {
44+
var body = CodeBlockItemListSyntax([
45+
CodeBlockItemSyntax(
46+
item: .expr(initializer.value)
47+
)
48+
])
49+
body.leadingTrivia = initializer.equal.trailingTrivia + body.leadingTrivia
50+
body.trailingTrivia += .space
51+
codeBlockSyntax = body
52+
}
53+
54+
let newBinding =
55+
binding
56+
.with(\.initializer, nil)
57+
.with(
58+
\.accessorBlock,
59+
AccessorBlockSyntax(
60+
accessors: .getter(codeBlockSyntax)
61+
)
62+
)
63+
64+
let newBindingSpecifier =
65+
syntax.bindingSpecifier
66+
.with(\.tokenKind, .keyword(.var))
67+
68+
return
69+
syntax
70+
.with(\.bindingSpecifier, newBindingSpecifier)
71+
.with(\.bindings, PatternBindingListSyntax([newBinding]))
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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() /* some text */ }
23+
"""
24+
25+
let expected: DeclSyntax = """
26+
let defaultColor: Color = Color() /* some text */
27+
"""
28+
29+
try assertRefactorConvert(baseline, expected: expected)
30+
}
31+
32+
func testToStoredWithReturnStatement() throws {
33+
let baseline: DeclSyntax = """
34+
var defaultColor: Color {
35+
return Color()
36+
}
37+
"""
38+
39+
let expected: DeclSyntax = """
40+
let defaultColor: Color = Color()
41+
"""
42+
43+
try assertRefactorConvert(baseline, expected: expected)
44+
}
45+
46+
func testToStoredWithReturnStatementAndTrailingComment() throws {
47+
let baseline: DeclSyntax = """
48+
var defaultColor: Color {
49+
return Color() /* some text */
50+
}
51+
"""
52+
53+
let expected: DeclSyntax = """
54+
let defaultColor: Color = Color() /* some text */
55+
"""
56+
57+
try assertRefactorConvert(baseline, expected: expected)
58+
}
59+
60+
func testToStoredWithReturnStatementAndTrailingCommentOnNewLine() throws {
61+
let baseline: DeclSyntax = """
62+
var defaultColor: Color {
63+
return Color()
64+
/* some text */
65+
}
66+
"""
67+
68+
let expected: DeclSyntax = """
69+
let defaultColor: Color = Color()
70+
/* some text */
71+
"""
72+
73+
try assertRefactorConvert(baseline, expected: expected)
74+
}
75+
76+
func testToStoredWithMultipleStatementsInAccessor() throws {
77+
let baseline: DeclSyntax = """
78+
var defaultColor: Color {
79+
let color = Color()
80+
return color
81+
}
82+
"""
83+
84+
let expected: DeclSyntax = """
85+
let defaultColor: Color = {
86+
let color = Color()
87+
return color
88+
}()
89+
"""
90+
91+
try assertRefactorConvert(baseline, expected: expected)
92+
}
93+
94+
func testToStoredWithMultipleStatementsInAccessorAndTrailingCommentsOnNewLine() throws {
95+
let baseline: DeclSyntax = """
96+
var defaultColor: Color {
97+
let color = Color()
98+
return color
99+
// returns color
100+
}
101+
"""
102+
103+
let expected: DeclSyntax = """
104+
let defaultColor: Color = {
105+
let color = Color()
106+
return color
107+
// returns color
108+
}()
109+
"""
110+
111+
try assertRefactorConvert(baseline, expected: expected)
112+
}
113+
114+
func testToStoredWithMultipleStatementsInAccessorAndLeadingComments() throws {
115+
let baseline: DeclSyntax = """
116+
var defaultColor: Color { // returns color
117+
let color = Color()
118+
return color
119+
}
120+
"""
121+
122+
let expected: DeclSyntax = """
123+
let defaultColor: Color = { // returns color
124+
let color = Color()
125+
return color
126+
}()
127+
"""
128+
129+
try assertRefactorConvert(baseline, expected: expected)
130+
}
131+
132+
func testToStoreWithSeparatingComments() throws {
133+
let baseline: DeclSyntax = """
134+
var x: Int {
135+
return
136+
/* One */ 1
137+
}
138+
"""
139+
140+
let expected: DeclSyntax = """
141+
let x: Int =
142+
/* One */ 1
143+
"""
144+
145+
try assertRefactorConvert(baseline, expected: expected)
146+
}
147+
}
148+
149+
fileprivate func assertRefactorConvert(
150+
_ callDecl: DeclSyntax,
151+
expected: DeclSyntax?,
152+
file: StaticString = #filePath,
153+
line: UInt = #line
154+
) throws {
155+
try assertRefactor(
156+
callDecl,
157+
context: (),
158+
provider: ConvertComputedPropertyToStored.self,
159+
expected: expected,
160+
file: file,
161+
line: line
162+
)
163+
}

0 commit comments

Comments
 (0)