diff --git a/Fixtures/SwiftFixIt/SwiftFixItPackage/Package.swift b/Fixtures/SwiftFixIt/SwiftFixItPackage/Package.swift new file mode 100644 index 00000000000..f655e92ec14 --- /dev/null +++ b/Fixtures/SwiftFixIt/SwiftFixItPackage/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let package = Package( + name: "SwiftFixItPackage", + targets: [ + .target(name: "Diagnostics", path: "Sources", exclude: ["Fixed"]), + ] +) diff --git a/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/first.swift b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/first.swift new file mode 100644 index 00000000000..f80dbb93fc1 --- /dev/null +++ b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/first.swift @@ -0,0 +1,12 @@ +func foo(_: any P, _: any P) {} + +func throwing() throws -> Int {} + +func foo() throws { + do { + _ = 0 + } + do { + _ = try throwing() + } +} diff --git a/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/second.swift b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/second.swift new file mode 100644 index 00000000000..f1e5ac3a839 --- /dev/null +++ b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/Fixed/second.swift @@ -0,0 +1,5 @@ +protocol P { + associatedtype A +} + +func bar(_: any P) {} diff --git a/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/first.swift b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/first.swift new file mode 100644 index 00000000000..473bf6ac67b --- /dev/null +++ b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/first.swift @@ -0,0 +1,12 @@ +func foo(_: P, _: P) {} + +func throwing() throws -> Int {} + +func foo() throws { + do { + var x = 0 + } + do { + let x = throwing() + } +} diff --git a/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/second.swift b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/second.swift new file mode 100644 index 00000000000..5b5c2835c57 --- /dev/null +++ b/Fixtures/SwiftFixIt/SwiftFixItPackage/Sources/second.swift @@ -0,0 +1,5 @@ +protocol P { + associatedtype A +} + +func bar(_: P) {} diff --git a/Package.swift b/Package.swift index e989267b4dd..fb3cb1a2685 100644 --- a/Package.swift +++ b/Package.swift @@ -588,6 +588,7 @@ let package = Package( "Workspace", "XCBuildSupport", "SwiftBuildSupport", + "SwiftFixIt", ] + swiftSyntaxDependencies(["SwiftIDEUtils"]), exclude: ["CMakeLists.txt", "README.md"], swiftSettings: swift6CompatibleExperimentalFeatures + [ @@ -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 diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index 9f644e3d326..e7a3a8ea773 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -40,6 +40,7 @@ add_library(Commands Snippets/Card.swift Snippets/Colorful.swift SwiftBuildCommand.swift + SwiftFixItCommand.swift SwiftRunCommand.swift SwiftTestCommand.swift CommandWorkspaceDelegate.swift diff --git a/Sources/Commands/SwiftFixitCommand.swift b/Sources/Commands/SwiftFixitCommand.swift new file mode 100644 index 00000000000..09e269cead4 --- /dev/null +++ b/Sources/Commands/SwiftFixitCommand.swift @@ -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() + } +} diff --git a/Sources/_InternalTestSupport/SwiftPMProduct.swift b/Sources/_InternalTestSupport/SwiftPMProduct.swift index 959421b6566..4d56bd037f7 100644 --- a/Sources/_InternalTestSupport/SwiftPMProduct.swift +++ b/Sources/_InternalTestSupport/SwiftPMProduct.swift @@ -29,6 +29,7 @@ public enum SwiftPM { case Run case experimentalSDK case sdk + case fixit } extension SwiftPM { @@ -49,6 +50,8 @@ extension SwiftPM { return "swift-experimental-sdk" case .sdk: return "swift-sdk" + case .fixit: + return "swift-fixit" } } diff --git a/Sources/swift-fixit/CMakeLists.txt b/Sources/swift-fixit/CMakeLists.txt new file mode 100644 index 00000000000..1661ebc6254 --- /dev/null +++ b/Sources/swift-fixit/CMakeLists.txt @@ -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) diff --git a/Sources/swift-fixit/Entrypoint.swift b/Sources/swift-fixit/Entrypoint.swift new file mode 100644 index 00000000000..9935eb97a8c --- /dev/null +++ b/Sources/swift-fixit/Entrypoint.swift @@ -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() + } +} diff --git a/Sources/swift-package-manager/SwiftPM.swift b/Sources/swift-package-manager/SwiftPM.swift index 3268423b983..cba6b5a1858 100644 --- a/Sources/swift-package-manager/SwiftPM.swift +++ b/Sources/swift-package-manager/SwiftPM.swift @@ -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)")") } diff --git a/Tests/CommandsTests/SwiftFixItCommandTests.swift b/Tests/CommandsTests/SwiftFixItCommandTests.swift new file mode 100644 index 00000000000..bdbf1ed27bb --- /dev/null +++ b/Tests/CommandsTests/SwiftFixItCommandTests.swift @@ -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) + ) + } + } + } +}