Skip to content

Commit 098e9b3

Browse files
antiglutenVladimir Gusev
authored and
Vladimir Gusev
committed
Code action to convert between computed property and stored property
1 parent fa81bf5 commit 098e9b3

5 files changed

+519
-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,96 @@
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 body.count == 1, 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+
return refactored(
64+
InitializerClauseSyntax(
65+
leadingTrivia: accessorBlock.leftBrace.trivia,
66+
equal: .equalToken(trailingTrivia: trailingTrivia),
67+
value: expression,
68+
trailingTrivia: accessorBlock.rightBrace.trivia.droppingTrailingWhitespace
69+
)
70+
)
71+
} else if var item = item.as(ExprSyntax.self) {
72+
item.trailingTrivia = item.trailingTrivia.droppingTrailingWhitespace
73+
return refactored(
74+
InitializerClauseSyntax(
75+
equal: .equalToken(trailingTrivia: .space),
76+
value: item,
77+
trailingTrivia: accessorBlock.trailingTrivia
78+
)
79+
)
80+
}
81+
82+
return nil
83+
}
84+
}
85+
86+
fileprivate extension TokenSyntax {
87+
var trivia: Trivia {
88+
return leadingTrivia + trailingTrivia
89+
}
90+
}
91+
92+
fileprivate extension Trivia {
93+
var droppingTrailingWhitespace: Trivia {
94+
return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed())
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
var codeBlockSyntax: CodeBlockItemListSyntax
26+
27+
if let functionExpression = initializer.value.as(FunctionCallExprSyntax.self),
28+
let closureExpression = functionExpression.calledExpression.as(ClosureExprSyntax.self)
29+
{
30+
guard functionExpression.arguments.isEmpty else { return nil }
31+
32+
codeBlockSyntax = closureExpression.statements
33+
codeBlockSyntax.leadingTrivia =
34+
closureExpression.leftBrace.leadingTrivia + closureExpression.leftBrace.trailingTrivia
35+
+ codeBlockSyntax.leadingTrivia
36+
codeBlockSyntax.trailingTrivia +=
37+
closureExpression.trailingTrivia + closureExpression.rightBrace.leadingTrivia
38+
+ closureExpression.rightBrace.trailingTrivia + functionExpression.trailingTrivia
39+
} else {
40+
var body = CodeBlockItemListSyntax([
41+
CodeBlockItemSyntax(
42+
item: .expr(initializer.value)
43+
)
44+
])
45+
body.leadingTrivia = initializer.equal.trailingTrivia + body.leadingTrivia
46+
body.trailingTrivia += .space
47+
codeBlockSyntax = body
48+
}
49+
50+
let newBinding =
51+
binding
52+
.with(\.initializer, nil)
53+
.with(
54+
\.accessorBlock,
55+
AccessorBlockSyntax(
56+
accessors: .getter(codeBlockSyntax)
57+
)
58+
)
59+
60+
let newBindingSpecifier =
61+
syntax.bindingSpecifier
62+
.with(\.tokenKind, .keyword(.var))
63+
64+
return
65+
syntax
66+
.with(\.bindingSpecifier, newBindingSpecifier)
67+
.with(\.bindings, PatternBindingListSyntax([newBinding]))
68+
}
69+
}
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)