Skip to content

Add SwiftRefactor Library #1049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions Examples/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ let package = Package(
products: [
.executable(name: "AddOneToIntegerLiterals", targets: ["AddOneToIntegerLiterals"]),
.executable(name: "CodeGenerationUsingSwiftSyntaxBuilder", targets: ["CodeGenerationUsingSwiftSyntaxBuilder"]),
.executable(name: "MigrateToNewIfLetSyntax", targets: ["MigrateToNewIfLetSyntax"]),
],
dependencies: [
.package(path: "../"),
Expand All @@ -23,24 +22,15 @@ let package = Package(
.product(name: "SwiftSyntax", package: "swift-syntax"),
],
path: ".",
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift", "MigrateToNewIfLetSyntax.swift"]
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift"]
),
.executableTarget(
name: "CodeGenerationUsingSwiftSyntaxBuilder",
dependencies: [
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
],
path: ".",
exclude: ["README.md", "AddOneToIntegerLiterals.swift", "MigrateToNewIfLetSyntax.swift"]
),
.executableTarget(
name: "MigrateToNewIfLetSyntax",
dependencies: [
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
],
path: ".",
exclude: ["README.md", "CodeGenerationUsingSwiftSyntaxBuilder.swift", "AddOneToIntegerLiterals.swift"]
exclude: ["README.md", "AddOneToIntegerLiterals.swift"]
),
]
)
1 change: 0 additions & 1 deletion Examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Each example can be executed by navigating into this folder and running `swift r

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

## Some Example Usages

Expand Down
11 changes: 11 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let package = Package(
.library(name: "SwiftSyntaxParser", type: .static, targets: ["SwiftSyntaxParser"]),
.library(name: "SwiftSyntaxBuilder", type: .static, targets: ["SwiftSyntaxBuilder"]),
.library(name: "_SwiftSyntaxMacros", type: .static, targets: ["_SwiftSyntaxMacros"]),
.library(name: "SwiftRefactor", type: .static, targets: ["SwiftRefactor"]),
],
targets: [
.target(
Expand Down Expand Up @@ -147,6 +148,11 @@ let package = Package(
exclude: [
"CMakeLists.txt",
]),
.target(
name: "SwiftRefactor",
dependencies: [
"SwiftSyntax", "SwiftParser",
]),
.executableTarget(
name: "lit-test-helper",
dependencies: ["IDEUtils", "SwiftSyntax", "SwiftSyntaxParser"]
Expand Down Expand Up @@ -199,6 +205,11 @@ let package = Package(
dependencies: ["SwiftOperators", "_SwiftSyntaxTestSupport",
"SwiftParser"]
),
.testTarget(
name: "SwiftRefactorTest",
dependencies: [
"SwiftRefactor", "SwiftSyntaxBuilder", "_SwiftSyntaxTestSupport",
]),
]
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import Foundation
import SwiftSyntax
import SwiftParser

/// MigrateToNewIfLetSyntax will visit each `if` statement in the syntax tree
/// replacing all "old style" optional bindings by the new shorter syntax available
/// since Swift 5.7.
/// ``MigrateToNewIfLetSyntax`` will visit each if statement in the Syntax tree, and
/// checks if the there is an if condition which is of the pre Swift 5.7 "if-let-style"
/// and rewrites it to the new one.
///
/// For example, it will turn:
/// ```
/// - Seealso: https://github.com/apple/swift-evolution/blob/main/proposals/0345-if-let-shorthand.md
///
/// ## Before
///
/// ```swift
/// if let foo = foo {
/// ...
/// // ...
/// }
/// ```
/// into:
/// ```
///
/// ## After
///
/// ```swift
/// if let foo {
/// ...
/// // ...
/// }
class MigrateToNewIfLetSyntax: SyntaxRewriter {
// Visit all `if` statements.
override func visit(_ node: IfStmtSyntax) -> StmtSyntax {
public struct MigrateToNewIfLetSyntax: RefactoringProvider {
public static func refactor(syntax node: IfStmtSyntax, in context: ()) -> StmtSyntax? {
// Visit all conditions in the node.
let newConditions = node.conditions.enumerated().map { (index, condition) in
var conditionCopy = condition
// Check if the condition is an optional binding ...
if var binding = condition.condition.as(OptionalBindingConditionSyntax.self),
// ... and has an initializer ...
let initializer = binding.initializer,
// ... that binds an identifier (and not a tuple) ...
let bindingIdentifier = binding.pattern.as(IdentifierPatternSyntax.self),
// ... and has an initializer that is also an identifier ...
let initializerIdentifier = binding.initializer?.value.as(IdentifierExprSyntax.self),
// ... and both sides of the assignment are the same identifiers.
binding.pattern.withoutTrivia().description == initializer.value.withoutTrivia().description {
bindingIdentifier.identifier.text == initializerIdentifier.identifier.text {
// Remove the initializer ...
binding.initializer = nil
// ... and remove whitespace before the comma (in `if` statements with multiple conditions).
Expand All @@ -42,15 +47,3 @@ class MigrateToNewIfLetSyntax: SyntaxRewriter {
return StmtSyntax(node.withConditions(ConditionElementListSyntax(newConditions)))
}
}

@main
struct Main {
static func main() throws {
let file = CommandLine.arguments[1]
let url = URL(fileURLWithPath: file)
let source = try String(contentsOf: url, encoding: .utf8)
let sourceFile = Parser.parse(source: source)
let rewritten = MigrateToNewIfLetSyntax().visit(sourceFile)
print(rewritten)
}
}
74 changes: 74 additions & 0 deletions Sources/SwiftRefactor/RefactoringProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import SwiftSyntax

/// A type that transforms syntax to provide a (context-sensitive)
/// refactoring.
///
/// A type conforming to the `RefactoringProvider` protocol defines the
/// a refactoring action against a family of Swift syntax trees.
///
/// Refactoring
/// ===========
///
/// Refactoring is the act of transforming source code to be more effective.
/// A refactoring does not affect the semantics of code it is transforming.
/// Rather, it makes that code easier to read and reason about.
///
/// Code Transformation
/// ===================
///
/// Refactoring is expressed as structural transformations of Swift
/// syntax trees. The SwiftSyntax API provides a natural, easy-to-use,
/// and compositional set of updates to the syntax tree. For example, a
/// refactoring action that wishes to exchange the leading trivia of a node
/// would call `withLeadingTrivia(_:)` against its input syntax and return
/// the resulting syntax node. For compound syntax nodes, entire sub-trees
/// can be added, exchanged, or removed by calling the corresponding `with`
/// API.
///
/// - Note: The syntax trees returned by SwiftSyntax are immutable: any
/// transformation made against the tree results in a distinct tree.
///
/// Handling Malformed Syntax
/// =========================
///
/// A refactoring provider cannot assume that the syntax it is given is
/// neessarily well-formed. As the SwiftSyntax library is capable of recovering
/// from a variety of erroneous inputs, a refactoring provider has to be
/// prepared to fail gracefully as well. Many refactoring providers follow a
/// common validation pattern that "preflights" the refactoring by testing the
/// structure of the provided syntax nodes. If the tests fail, the refactoring
/// provider exits early by returning `nil`. It is recommended that refactoring
/// actions fail as quickly as possible to give any associated tooling
/// space to recover as well.
public protocol RefactoringProvider {
/// The type of syntax this refactoring action accepts.
associatedtype Input: SyntaxProtocol = SourceFileSyntax
/// The type of syntax this refactorign action returns.
associatedtype Output: SyntaxProtocol = SourceFileSyntax
/// Contextual information used by the refactoring action.
associatedtype Context = Void

/// Perform the refactoring action on the provided syntax node.
///
/// - Parameters:
/// - syntax: The syntax to transform.
/// - context: Contextual information used by the refactoring action.
/// - Returns: The result of applying the refactoring action, or `nil` if the
/// action could not be performed.
static func refactor(syntax: Self.Input, in context: Self.Context) -> Self.Output?
}

extension RefactoringProvider where Context == Void {
/// Perform the refactoring action on the provided syntax node.
///
/// This method provides a convenient way to invoke a refactoring action that
/// requires no context.
///
/// - Parameters:
/// - syntax: The syntax to transform.
/// - Returns: The result of applying the refactoring action, or `nil` if the
/// action could not be performed.
public static func refactor(syntax: Self.Input) -> Self.Output? {
return self.refactor(syntax: syntax, in: ())
}
}
118 changes: 118 additions & 0 deletions Tests/SwiftRefactorTest/MigrateToNewIfLetSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 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 MigrateToNewIfLetSyntaxTest: XCTestCase {
func testRefactoring() throws {
let baselineSyntax: StmtSyntax = """
if let x = x {}
"""

let expectedSyntax: StmtSyntax = """
if let x {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(expected.description, refactored.description)
}

func testIdempotence() throws {
let baselineSyntax: StmtSyntax = """
if let x = x {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))
let refactoredAgain = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(refactored.description, refactoredAgain.description)
}

func testMultiBinding() throws {
let baselineSyntax: StmtSyntax = """
if let x = x, var y = y, let z = z {}
"""

let expectedSyntax: StmtSyntax = """
if let x, var y, let z {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(expected.description, refactored.description)
}

func testMixedBinding() throws {
let baselineSyntax: StmtSyntax = """
if let x = x, var y = x, let z = y.w {}
"""

let expectedSyntax: StmtSyntax = """
if let x, var y = x, let z = y.w {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(expected.description, refactored.description)
}

func testConditions() throws {
let baselineSyntax: StmtSyntax = """
if let x = x + 1, x == x, !x {}
"""

let expectedSyntax: StmtSyntax = """
if let x = x + 1, x == x, !x {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(expected.description, refactored.description)
}

func testWhitespaceNormalization() throws {
let baselineSyntax: StmtSyntax = """
if let x = x , let y = y {}
"""

let expectedSyntax: StmtSyntax = """
if let x, let y {}
"""

let baseline = try XCTUnwrap(baselineSyntax.as(IfStmtSyntax.self))
let expected = try XCTUnwrap(expectedSyntax.as(IfStmtSyntax.self))

let refactored = try XCTUnwrap(MigrateToNewIfLetSyntax.refactor(syntax: baseline))

AssertStringsEqualWithDiff(expected.description, refactored.description)
}
}
1 change: 0 additions & 1 deletion build-script.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,6 @@ def build_command(args: argparse.Namespace) -> None:
# Build examples
builder.buildExample("AddOneToIntegerLiterals")
builder.buildExample("CodeGenerationUsingSwiftSyntaxBuilder")
builder.buildExample("MigrateToNewIfLetSyntax")
except subprocess.CalledProcessError as e:
fail_for_called_process_error("Building SwiftSyntax failed", e)

Expand Down