SwiftSyntax By Example

In this tutorial you’ll explore the SwiftSyntax API by writing a tool that formats and sorts the import statements in a Swift file.

SwiftSyntax By Example.tutorial

You’ll start by using the Swift parser to parse code into a syntax tree. Then you’ll use syntax transformation APIs to analyze the structure of the document. Finally, you’ll write out fully-formatted Swift code as output.

Create a New Executable Package using SwiftSyntax

Create a new executable package that uses SwiftSyntax.

We’ll keep our formatter tool contained to the inputformatter.swift file generated by the package. You will not need to add any new files during this tutorial.

  1. Open Xcode and choose File > New > Package, name your package “importformatter”, and choose a place for it on disk.

    A screenshot of a new Xcode new package project, displaying the UI for saving the package to the users's Documents folder with the name importformatter
  2. Select the Package.swift file in the file navigator.

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
          name: "importformatter"
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step1.swift
  3. Change the .library entry for importformatter to a .executable entry, and change the .target entry to a .executableTarget.

    The completed Package.swift file should look like this:

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .executable(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
          name: "importformatter"
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step2.swift

Configure Your Package

Configure your package and get started with a command line application.

  1. Open the Package.swift file created in the previous section.

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .executable(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
          name: "importformatter"
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step2.swift
  2. Add the swift-syntax package to the dependencies section of your Package.swift file.

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .executable(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main")
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
          name: "importformatter"
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step3.swift
  3. Add the SwiftSyntax and SwiftParser products as dependencies of the importformatter executable target.

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .executable(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main")
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
          name: "importformatter",
          dependencies: [
            .product(name: "SwiftSyntax", package: "swift-syntax"),
            .product(name: "SwiftParser", package: "swift-syntax"),
          ]
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step4.swift
  4. Add a macOS 10.15 platform constraint to the platforms section of your Package.swift file.

    Package.swift
    // swift-tools-version: 5.9
    // The swift-tools-version declares the minimum version of Swift required to build this package.
    
    import PackageDescription
    
    let package = Package(
      name: "importformatter",
      platforms: [
        .macOS(.v10_15)
      ],
      products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .executable(
          name: "importformatter",
          targets: ["importformatter"]
        )
      ],
      dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main")
      ],
      targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
          name: "importformatter",
          dependencies: [
            .product(name: "SwiftSyntax", package: "swift-syntax"),
            .product(name: "SwiftParser", package: "swift-syntax"),
          ]
        ),
        .testTarget(
          name: "importformatterTests",
          dependencies: ["importformatter"]
        ),
      ]
    )
    Package.step5.swift

Getting Started with SwiftSyntax

You’ll use SwiftSyntax to fill out the functionality of the formatter. The tool sends its output to standard out by printing the syntax tree. You should run the formatter on one or more test files while you follow this tutorial and observe the resulting output.

  1. In the Xcode file navigator, select the importformatter.swift.

    importformatter.swift
    import Foundation
    
    @main struct ImportFormatter {
      static func main() {
    
      }
    }
    Formatter.step1.swift
  2. Using the provided code, set up a bare-bones command line application in importformatter.swift.

    importformatter.swift
    import Foundation
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        /* Formatter here */
      }
    }
    Formatter.step2.swift
  3. Add an import of the SwiftSyntax and SwiftParser libraries in importformatter.swift

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        /* Formatter here */
      }
    }
    Formatter.step3.swift
  4. Call Parser.parse to parse the input file as a SourceFileSyntax value.

    At this point, your formatter tool is ready to run, even if it doesn’t actually format any code. Create a test swift file pass it to the formatter to make sure it gets printed to the console in Xcode. To configure the arguments to a command line application, enter the scheme editor with Product > Scheme > Edit Scheme. Under the ‘Arguments’ tab you can provide the full path to your test Swift file.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        /* format source file */
        return sourceFile
      }
    }
    Formatter.step4.swift
  5. Next, write a helper function that classifies code block items containing imports. To help keep track of the classified items, define an Item enum that has two cases: one for import declaration items and one for other items.

    Small helper functions and helper types like these help make your Swift code easier to read and reason about.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        return sourceFile
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        /* Classify items here */
      }
    }
    Formatter.step5.swift
  6. To implement the item classifier, map over the statements in the source file syntax node.

    The map sends the items with ImportDeclSyntax nodes to Item.import values, and all other items to Item.other values.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        return sourceFile
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step6.swift
  7. With the classifier function written, it’s time to implement the formatter itself. First, make sure that all of the imports appear at the start of the source file. Swift provides a handy method to let us group a related set of statements together: partition(by:). Calling this method on the classified items also returns the index that divides the imports from the other statements, which will come in handy later.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        return sourceFile
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step7.swift
  8. Next, sort the imports among each other by calling sort(by:) using the lexicographic comparison provided by Swift’s String.

    Notice that we don’t need to sort the entire array of statements, just the imports. The call to partition(by:) gave us precisely the range of the statements collection we actually need to sort!

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        return sourceFile
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step8.swift
  9. Now that the array of items is sorted and everything is in the right position, modify the source file to actually use those statements. Call the map method to extract the statements from the array of items.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        let formattedStatements = items.map { item in
          switch item {
          case .import(_, let statement):
            return statement
          case .other(let statement):
            return statement
          }
        }
        return sourceFile
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step9.swift
  10. Then use with(\.statements, <#new statements#>)* method to create a source file that contains the extracted array of statements.

    The syntax tree is immutable. The result of calling with is always a copy of a syntax tree node with the specified child element changed.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        let formattedStatements = items.map { item in
          switch item {
          case .import(_, let statement):
            return statement
          case .other(let statement):
            return statement
          }
        }
        return
          sourceFile
          .with(\.statements, CodeBlockItemListSyntax(formattedStatements))
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step10.swift

Cleaning Up The Syntax Tree

Now that you have the core of the formatter done, you’ll learn how to manipulate whitespace to clean up its output.

Syntax trees created with the Swift parser and SwiftSyntax provide a property called source fidelity. This means that all whitespace, comments, and even invisible bytes in the file are represented in the tree. Collectively, we refer to these entities as trivia.

In this section, you’ll use methods provided by SwiftSyntax to manipulate the trivia attached to syntax nodes.

  1. The formatter is nearly complete, but there’s still a pretty nasty set of bugs. Run the tool on the importformatter.swift file itself and observe its output carefully.

    The imports of SwiftParser and SwiftSyntax are jammed together with no whitespace between them. The import of Foundation has an extra leading newline.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        let formattedStatements = items.map { item in
          switch item {
          case .import(_, let statement):
            return statement
          case .other(let statement):
            return statement
          }
        }
        return
          sourceFile
          .with(\.statements, CodeBlockItemListSyntax(formattedStatements))
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step10.swift
  2. Normalize the whitespace of all the imports by calling the trivia manipulation functions with(\.leadingTrivia, _:) and with(\.trailingTrivia, _:) on import items.

    SwiftSyntax does not automatically format whitespace and trivia when items are moved around. SwiftSyntax provides a convenient way of manipulating whitespace by calling the with(\.leadingTrivia, _:) and with(\.trailingTrivia, _:) methods. By normalizing all of the whitespace to a single leading newline we can fix bug #1. And by removing all trailing whitespace we can fix bug #2.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        let formattedStatements = items.enumerated().map { (offset, item) in
          switch item {
          case .import(_, let statement):
            return
              statement
              .with(\.leadingTrivia, offset == 0 ? [] : .newline)
              .with(\.trailingTrivia, [])
          case .other(let statement):
            return statement
          }
        }
        return
          sourceFile
          .with(\.statements, CodeBlockItemListSyntax(formattedStatements))
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step11.swift
  3. Check your progress. Try re-running the formatter on the importformatter.swift file again. This time, notice that the import statements are not just sorted, but have whitespace corrected and normalized.

    There’s still some work to do here, though. Because we replaced all of the trivia, we’ve also removed any comments attached to the imports. As an added challenge, try to edit the trivia to preserve comments and only change whitespace.

    importformatter.swift
    import Foundation
    import SwiftParser
    import SwiftSyntax
    
    @main struct ImportFormatter {
      static func main() {
        guard CommandLine.arguments.count == 2 else {
          print("Not enough arguments!")
          return
        }
    
        let filePath = CommandLine.arguments[1]
        guard FileManager.default.fileExists(atPath: filePath) else {
          print("File doesn't exist at path: \(filePath)")
          return
        }
    
        guard let file = try? String(contentsOfFile: filePath) else {
          print("File at path isn't readable: \(filePath)")
          return
        }
    
        let formattedFile = ImportFormatter().formatImports(in: file)
        print(formattedFile)
      }
    
      func formatImports(in file: String) -> SourceFileSyntax {
        let sourceFile = Parser.parse(source: file)
        var items = classifyItems(in: sourceFile)
        let pivotPoint = items.partition { item in
          switch item {
          case .import(_, _):
            return false
          case .other(_):
            return true
          }
        }
        items[..<pivotPoint]
          .sort { lhs, rhs in
            guard
              case let .import(lhsImport, _) = lhs,
              case let .import(rhsImport, _) = rhs
            else {
              fatalError("Partition must only contain import items!")
            }
            return lhsImport.path.description < rhsImport.path.description
          }
        let formattedStatements = items.enumerated().map { (offset, item) in
          switch item {
          case .import(_, let statement):
            return
              statement
              .with(\.leadingTrivia, offset == 0 ? [] : .newline)
              .with(\.trailingTrivia, [])
          case .other(let statement):
            return statement
          }
        }
        return
          sourceFile
          .with(\.statements, CodeBlockItemListSyntax(formattedStatements))
      }
    
      enum Item {
        case `import`(ImportDeclSyntax, CodeBlockItemSyntax)
        case other(CodeBlockItemSyntax)
      }
    
      func classifyItems(in file: SourceFileSyntax) -> [Item] {
        file
          .statements
          .map { statement in
            if case .decl(let decl) = statement.item,
              let `import` = decl.as(ImportDeclSyntax.self)
            {
              return .import(`import`, statement)
            } else {
              return .other(statement)
            }
          }
      }
    }
    Formatter.step11.swift