Skip to content

Commit e44e18c

Browse files
authored
Merge pull request #1049 from CodaFi/refactorio
2 parents f2a64d9 + 3a872e1 commit e44e18c

File tree

7 files changed

+226
-42
lines changed

7 files changed

+226
-42
lines changed

Examples/Package.swift

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ let package = Package(
1010
products: [
1111
.executable(name: "AddOneToIntegerLiterals", targets: ["AddOneToIntegerLiterals"]),
1212
.executable(name: "CodeGenerationUsingSwiftSyntaxBuilder", targets: ["CodeGenerationUsingSwiftSyntaxBuilder"]),
13-
.executable(name: "MigrateToNewIfLetSyntax", targets: ["MigrateToNewIfLetSyntax"]),
1413
],
1514
dependencies: [
1615
.package(path: "../"),
@@ -23,24 +22,15 @@ let package = Package(
2322
.product(name: "SwiftSyntax", package: "swift-syntax"),
2423
],
2524
path: ".",
26-
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift", "MigrateToNewIfLetSyntax.swift"]
25+
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift"]
2726
),
2827
.executableTarget(
2928
name: "CodeGenerationUsingSwiftSyntaxBuilder",
3029
dependencies: [
3130
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
3231
],
3332
path: ".",
34-
exclude: ["README.md", "AddOneToIntegerLiterals.swift", "MigrateToNewIfLetSyntax.swift"]
35-
),
36-
.executableTarget(
37-
name: "MigrateToNewIfLetSyntax",
38-
dependencies: [
39-
.product(name: "SwiftParser", package: "swift-syntax"),
40-
.product(name: "SwiftSyntax", package: "swift-syntax"),
41-
],
42-
path: ".",
43-
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift", "AddOneToIntegerLiterals.swift"]
33+
exclude: ["README.md", "AddOneToIntegerLiterals.swift"]
4434
),
4535
]
4636
)

Examples/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Each example can be executed by navigating into this folder and running `swift r
44

55
- [AddOneToIntegerLiterals](AddOneToIntegerLiterals.swift): Command line tool to add 1 to every integer literal in a source file
66
- [CodeGenerationUsingSwiftSyntaxBuilder](CodeGenerationUsingSwiftSyntaxBuilder.swift): Code-generate a simple source file using SwiftSyntaxBuilder
7-
- [MigrateToNewIfLetSyntax](MigrateToNewIfLetSyntax.swift): Command line tool to transform optional bindings in `if` statements to the new shorthand syntax
87

98
## Some Example Usages
109

Package.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ let package = Package(
4141
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
4242
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
4343
.library(name: "_SwiftSyntaxMacros", type: .static, targets: ["_SwiftSyntaxMacros"]),
44+
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
4445
],
4546
targets: [
4647
.target(
@@ -147,6 +148,11 @@ let package = Package(
147148
exclude: [
148149
"CMakeLists.txt",
149150
]),
151+
.target(
152+
name: "SwiftRefactor",
153+
dependencies: [
154+
"SwiftSyntax", "SwiftParser",
155+
]),
150156
.executableTarget(
151157
name: "lit-test-helper",
152158
dependencies: ["IDEUtils", "SwiftSyntax", "SwiftSyntaxParser"]
@@ -199,6 +205,11 @@ let package = Package(
199205
dependencies: ["SwiftOperators", "_SwiftSyntaxTestSupport",
200206
"SwiftParser"]
201207
),
208+
.testTarget(
209+
name: "SwiftRefactorTest",
210+
dependencies: [
211+
"SwiftRefactor", "SwiftSyntaxBuilder", "_SwiftSyntaxTestSupport",
212+
]),
202213
]
203214
)
204215

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
1-
import Foundation
21
import SwiftSyntax
32
import SwiftParser
43

5-
/// MigrateToNewIfLetSyntax will visit each `if` statement in the syntax tree
6-
/// replacing all "old style" optional bindings by the new shorter syntax available
7-
/// since Swift 5.7.
4+
/// ``MigrateToNewIfLetSyntax`` will visit each if statement in the Syntax tree, and
5+
/// checks if the there is an if condition which is of the pre Swift 5.7 "if-let-style"
6+
/// and rewrites it to the new one.
87
///
9-
/// For example, it will turn:
10-
/// ```
8+
/// - Seealso: https://github.com/apple/swift-evolution/blob/main/proposals/0345-if-let-shorthand.md
9+
///
10+
/// ## Before
11+
///
12+
/// ```swift
1113
/// if let foo = foo {
12-
/// ...
14+
/// // ...
1315
/// }
1416
/// ```
15-
/// into:
16-
/// ```
17+
///
18+
/// ## After
19+
///
20+
/// ```swift
1721
/// if let foo {
18-
/// ...
22+
/// // ...
1923
/// }
20-
class MigrateToNewIfLetSyntax: SyntaxRewriter {
21-
// Visit all `if` statements.
22-
override func visit(_ node: IfStmtSyntax) -> StmtSyntax {
24+
public struct MigrateToNewIfLetSyntax: RefactoringProvider {
25+
public static func refactor(syntax node: IfStmtSyntax, in context: ()) -> StmtSyntax? {
2326
// Visit all conditions in the node.
2427
let newConditions = node.conditions.enumerated().map { (index, condition) in
2528
var conditionCopy = condition
2629
// Check if the condition is an optional binding ...
2730
if var binding = condition.condition.as(OptionalBindingConditionSyntax.self),
28-
// ... and has an initializer ...
29-
let initializer = binding.initializer,
31+
// ... that binds an identifier (and not a tuple) ...
32+
let bindingIdentifier = binding.pattern.as(IdentifierPatternSyntax.self),
33+
// ... and has an initializer that is also an identifier ...
34+
let initializerIdentifier = binding.initializer?.value.as(IdentifierExprSyntax.self),
3035
// ... and both sides of the assignment are the same identifiers.
31-
binding.pattern.withoutTrivia().description == initializer.value.withoutTrivia().description {
36+
bindingIdentifier.identifier.text == initializerIdentifier.identifier.text {
3237
// Remove the initializer ...
3338
binding.initializer = nil
3439
// ... and remove whitespace before the comma (in `if` statements with multiple conditions).
@@ -42,15 +47,3 @@ class MigrateToNewIfLetSyntax: SyntaxRewriter {
4247
return StmtSyntax(node.withConditions(ConditionElementListSyntax(newConditions)))
4348
}
4449
}
45-
46-
@main
47-
struct Main {
48-
static func main() throws {
49-
let file = CommandLine.arguments[1]
50-
let url = URL(fileURLWithPath: file)
51-
let source = try String(contentsOf: url, encoding: .utf8)
52-
let sourceFile = Parser.parse(source: source)
53-
let rewritten = MigrateToNewIfLetSyntax().visit(sourceFile)
54-
print(rewritten)
55-
}
56-
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SwiftSyntax
2+
3+
/// A type that transforms syntax to provide a (context-sensitive)
4+
/// refactoring.
5+
///
6+
/// A type conforming to the `RefactoringProvider` protocol defines the
7+
/// a refactoring action against a family of Swift syntax trees.
8+
///
9+
/// Refactoring
10+
/// ===========
11+
///
12+
/// Refactoring is the act of transforming source code to be more effective.
13+
/// A refactoring does not affect the semantics of code it is transforming.
14+
/// Rather, it makes that code easier to read and reason about.
15+
///
16+
/// Code Transformation
17+
/// ===================
18+
///
19+
/// Refactoring is expressed as structural transformations of Swift
20+
/// syntax trees. The SwiftSyntax API provides a natural, easy-to-use,
21+
/// and compositional set of updates to the syntax tree. For example, a
22+
/// refactoring action that wishes to exchange the leading trivia of a node
23+
/// would call `withLeadingTrivia(_:)` against its input syntax and return
24+
/// the resulting syntax node. For compound syntax nodes, entire sub-trees
25+
/// can be added, exchanged, or removed by calling the corresponding `with`
26+
/// API.
27+
///
28+
/// - Note: The syntax trees returned by SwiftSyntax are immutable: any
29+
/// transformation made against the tree results in a distinct tree.
30+
///
31+
/// Handling Malformed Syntax
32+
/// =========================
33+
///
34+
/// A refactoring provider cannot assume that the syntax it is given is
35+
/// neessarily well-formed. As the SwiftSyntax library is capable of recovering
36+
/// from a variety of erroneous inputs, a refactoring provider has to be
37+
/// prepared to fail gracefully as well. Many refactoring providers follow a
38+
/// common validation pattern that "preflights" the refactoring by testing the
39+
/// structure of the provided syntax nodes. If the tests fail, the refactoring
40+
/// provider exits early by returning `nil`. It is recommended that refactoring
41+
/// actions fail as quickly as possible to give any associated tooling
42+
/// space to recover as well.
43+
public protocol RefactoringProvider {
44+
/// The type of syntax this refactoring action accepts.
45+
associatedtype Input: SyntaxProtocol = SourceFileSyntax
46+
/// The type of syntax this refactorign action returns.
47+
associatedtype Output: SyntaxProtocol = SourceFileSyntax
48+
/// Contextual information used by the refactoring action.
49+
associatedtype Context = Void
50+
51+
/// Perform the refactoring action on the provided syntax node.
52+
///
53+
/// - Parameters:
54+
/// - syntax: The syntax to transform.
55+
/// - context: Contextual information used by the refactoring action.
56+
/// - Returns: The result of applying the refactoring action, or `nil` if the
57+
/// action could not be performed.
58+
static func refactor(syntax: Self.Input, in context: Self.Context) -> Self.Output?
59+
}
60+
61+
extension RefactoringProvider where Context == Void {
62+
/// Perform the refactoring action on the provided syntax node.
63+
///
64+
/// This method provides a convenient way to invoke a refactoring action that
65+
/// requires no context.
66+
///
67+
/// - Parameters:
68+
/// - syntax: The syntax to transform.
69+
/// - Returns: The result of applying the refactoring action, or `nil` if the
70+
/// action could not be performed.
71+
public static func refactor(syntax: Self.Input) -> Self.Output? {
72+
return self.refactor(syntax: syntax, in: ())
73+
}
74+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 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+
17+
import XCTest
18+
import _SwiftSyntaxTestSupport
19+
20+
final class MigrateToNewIfLetSyntaxTest: XCTestCase {
21+
func testRefactoring() throws {
22+
let baselineSyntax: StmtSyntax = """
23+
if let x = x {}
24+
"""
25+
26+
let expectedSyntax: StmtSyntax = """
27+
if let x {}
28+
"""
29+
30+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
31+
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))
32+
33+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
34+
35+
AssertStringsEqualWithDiff(expected.description, refactored.description)
36+
}
37+
38+
func testIdempotence() throws {
39+
let baselineSyntax: StmtSyntax = """
40+
if let x = x {}
41+
"""
42+
43+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
44+
45+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
46+
let refactoredAgain = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
47+
48+
AssertStringsEqualWithDiff(refactored.description, refactoredAgain.description)
49+
}
50+
51+
func testMultiBinding() throws {
52+
let baselineSyntax: StmtSyntax = """
53+
if let x = x, var y = y, let z = z {}
54+
"""
55+
56+
let expectedSyntax: StmtSyntax = """
57+
if let x, var y, let z {}
58+
"""
59+
60+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
61+
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))
62+
63+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
64+
65+
AssertStringsEqualWithDiff(expected.description, refactored.description)
66+
}
67+
68+
func testMixedBinding() throws {
69+
let baselineSyntax: StmtSyntax = """
70+
if let x = x, var y = x, let z = y.w {}
71+
"""
72+
73+
let expectedSyntax: StmtSyntax = """
74+
if let x, var y = x, let z = y.w {}
75+
"""
76+
77+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
78+
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))
79+
80+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
81+
82+
AssertStringsEqualWithDiff(expected.description, refactored.description)
83+
}
84+
85+
func testConditions() throws {
86+
let baselineSyntax: StmtSyntax = """
87+
if let x = x + 1, x == x, !x {}
88+
"""
89+
90+
let expectedSyntax: StmtSyntax = """
91+
if let x = x + 1, x == x, !x {}
92+
"""
93+
94+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
95+
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))
96+
97+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
98+
99+
AssertStringsEqualWithDiff(expected.description, refactored.description)
100+
}
101+
102+
func testWhitespaceNormalization() throws {
103+
let baselineSyntax: StmtSyntax = """
104+
if let x = x , let y = y {}
105+
"""
106+
107+
let expectedSyntax: StmtSyntax = """
108+
if let x, let y {}
109+
"""
110+
111+
let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
112+
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))
113+
114+
let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
115+
116+
AssertStringsEqualWithDiff(expected.description, refactored.description)
117+
}
118+
}

build-script.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,6 @@ def build_command(args: argparse.Namespace) -> None:
766766
# Build examples
767767
builder.buildExample("AddOneToIntegerLiterals")
768768
builder.buildExample("CodeGenerationUsingSwiftSyntaxBuilder")
769-
builder.buildExample("MigrateToNewIfLetSyntax")
770769
except subprocess.CalledProcessError as e:
771770
fail_for_called_process_error("Building SwiftSyntax failed", e)
772771

0 commit comments

Comments
 (0)