Skip to content

[Commands] Prototype swift-fixit — a subcommand for deserializing diagnostics and applying fix-its #8576

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions Fixtures/SwiftFixIt/SwiftFixItPackage/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// swift-tools-version:5.9

import PackageDescription

let package = Package(
name: "SwiftFixItPackage",
targets: [
.target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]),
]
)
12 changes: 12 additions & 0 deletions Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/first.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
func foo(_: any P, _: any P) {}

func throwing() throws -> Int {}

func foo() throws {
do {
_ = 0
}
do {
_ = try throwing()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
protocol P {
associatedtype A
}

func bar(_: any P) {}
12 changes: 12 additions & 0 deletions Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/first.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
func foo(_: P, _: P) {}

func throwing() throws -> Int {}

func foo() throws {
do {
var x = 0
}
do {
let x = throwing()
}
}
5 changes: 5 additions & 0 deletions Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/second.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
protocol P {
associatedtype A
}

func bar(_: P) {}
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ let package = Package(
"Workspace",
"XCBuildSupport",
"SwiftBuildSupport",
"SwiftFixIt",
] + swiftSyntaxDependencies(["SwiftIDEUtils"]),
exclude: ["CMakeLists.txt", "README.md"],
swiftSettings: swift6CompatibleExperimentalFeatures + [
Expand Down Expand Up @@ -745,6 +746,12 @@ let package = Package(
"Workspace",
]
),
.executableTarget(
/** Deserializes diagnostics and applies fix-its */
name: "swift-fixit",
dependencies: ["Commands"],
exclude: ["CMakeLists.txt"]
),

// MARK: Support for Swift macros, should eventually move to a plugin-based solution

Expand Down
1 change: 1 addition & 0 deletions Sources/Commands/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ add_library(Commands
Snippets/Card.swift
Snippets/Colorful.swift
SwiftBuildCommand.swift
SwiftFixItCommand.swift
SwiftRunCommand.swift
SwiftTestCommand.swift
CommandWorkspaceDelegate.swift
Expand Down
66 changes: 66 additions & 0 deletions Sources/Commands/SwiftFixitCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import struct ArgumentParser.Argument
import protocol ArgumentParser.AsyncParsableCommand
import struct ArgumentParser.CommandConfiguration
import struct ArgumentParser.OptionGroup
import protocol ArgumentParser.ParsableArguments

import struct Basics.AbsolutePath
import var Basics.localFileSystem
import struct Basics.SwiftVersion

import struct CoreCommands.LoggingOptions

import struct SwiftFixIt.SwiftFixIt

private struct Options: ParsableArguments {
@OptionGroup(title: "Logging")
var logging: LoggingOptions

@Argument(
help: "",
completion: .file(extensions: [".dia"])
)
var diagnosticFiles: [AbsolutePath] = []
}

/// Deserializes `.dia` files and applies all fix-its in place.
package struct SwiftFixitCommand: AsyncParsableCommand {
package static var configuration = CommandConfiguration(
commandName: "fixit",
_superCommandName: "swift",
abstract: "Deserialize diagnostics and apply fix-its",
version: SwiftVersion.current.completeDisplayString,
helpNames: [
.short,
.long,
.customLong("help", withSingleDash: true),
]
)

@OptionGroup
fileprivate var options: Options

package init() {}

package func run() async throws {
if self.options.diagnosticFiles.isEmpty {
return
}

let swiftFixIt = try SwiftFixIt(diagnosticFiles: options.diagnosticFiles, fileSystem: localFileSystem)

try swiftFixIt.applyFixIts()
}
}
3 changes: 3 additions & 0 deletions Sources/_InternalTestSupport/SwiftPMProduct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum SwiftPM {
case Run
case experimentalSDK
case sdk
case fixit
}

extension SwiftPM {
Expand All @@ -49,6 +50,8 @@ extension SwiftPM {
return "swift-experimental-sdk"
case .sdk:
return "swift-sdk"
case .fixit:
return "swift-fixit"
}
}

Expand Down
18 changes: 18 additions & 0 deletions Sources/swift-fixit/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# This source file is part of the Swift open source project
#
# Copyright (c) 2025 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_executable(swift-fixit
Entrypoint.swift)
target_link_libraries(swift-test PRIVATE
Commands)

target_compile_options(swift-test PRIVATE
-parse-as-library)

install(TARGETS swift-fixit
RUNTIME DESTINATION bin)
20 changes: 20 additions & 0 deletions Sources/swift-fixit/Entrypoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Commands

@main
struct Entrypoint {
static func main() async {
await SwiftFixitCommand.main()
}
}
2 changes: 2 additions & 0 deletions Sources/swift-package-manager/SwiftPM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ struct SwiftPM {
await PackageCollectionsCommand.main()
case "swift-package-registry":
await PackageRegistryCommand.main()
case "swift-fixit":
await SwiftFixitCommand.main()
default:
fatalError("swift-package-manager launched with unexpected name: \(execName ?? "(unknown)")")
}
Expand Down
82 changes: 82 additions & 0 deletions Tests/CommandsTests/SwiftFixItCommandTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import _InternalTestSupport
import struct Basics.AbsolutePath
import var Basics.localFileSystem
@testable
import Commands
import class PackageModel.UserToolchain
import XCTest

final class FixItCommandTests: CommandsTestCase {
func testHelp() async throws {
let stdout = try await SwiftPM.fixit.execute(["-help"]).stdout

XCTAssert(stdout.contains("USAGE: swift fixit"), stdout)
XCTAssert(stdout.contains("-h, -help, --help"), stdout)
}

func testApplyFixIts() async throws {
try await fixture(name: "SwiftFixIt/SwiftFixItPackage") { fixturePath in
let sourcePaths: [AbsolutePath]
let fixedSourcePaths: [AbsolutePath]
do {
let sourcesPath = fixturePath.appending(components: "Sources")
let fixedSourcesPath = sourcesPath.appending("Fixed")

sourcePaths = try localFileSystem.getDirectoryContents(sourcesPath).filter { filename in
filename.hasSuffix(".swift")
}.sorted().map { filename in
sourcesPath.appending(filename)
}
fixedSourcePaths = try localFileSystem.getDirectoryContents(fixedSourcesPath).filter { filename in
filename.hasSuffix(".swift")
}.sorted().map { filename in
fixedSourcesPath.appending(filename)
}
}

XCTAssertEqual(sourcePaths.count, fixedSourcePaths.count)

let targetName = "Diagnostics"

_ = try? await executeSwiftBuild(fixturePath, extraArgs: ["--target", targetName])

do {
let artifactsPath = try fixturePath.appending(
components: ".build",
UserToolchain.default.targetTriple.platformBuildPathComponent,
"debug",
"\(targetName).build"
)
let diaFilePaths = try localFileSystem.getDirectoryContents(artifactsPath).filter { filename in
// Ignore "*.emit-module.dia".
filename.split(".").1 == "dia"
}.map { filename in
artifactsPath.appending(component: filename).pathString
}

XCTAssertEqual(sourcePaths.count, diaFilePaths.count)

_ = try await SwiftPM.fixit.execute(diaFilePaths)
}

for (sourcePath, fixedSourcePath) in zip(sourcePaths, fixedSourcePaths) {
try XCTAssertEqual(
localFileSystem.readFileContents(sourcePath),
localFileSystem.readFileContents(fixedSourcePath)
)
}
}
}
}