Skip to content

Implement SwiftFixIt — a library for deserializing diagnostics and applying fix-its #8585

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
May 2, 2025
Merged
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
19 changes: 19 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -304,6 +304,21 @@ let package = Package(
]
),

.target(
/** API for deserializing diagnostics and applying fix-its */
name: "SwiftFixIt",
dependencies: [
"Basics",
.product(name: "TSCBasic", package: "swift-tools-support-core"),
] + swiftSyntaxDependencies(
["SwiftDiagnostics", "SwiftIDEUtils", "SwiftParser", "SwiftSyntax"]
),
exclude: ["CMakeLists.txt"],
swiftSettings: commonExperimentalFeatures + [
.unsafeFlags(["-static"]),
]
),

// MARK: Project Model

.target(
@@ -916,6 +931,10 @@ let package = Package(
dependencies: ["SourceControl", "_InternalTestSupport"],
exclude: ["Inputs/TestRepo.tgz"]
),
.testTarget(
name: "SwiftFixItTests",
dependencies: ["SwiftFixIt", "_InternalTestSupport"]
),
.testTarget(
name: "XCBuildSupportTests",
dependencies: ["XCBuildSupport", "_InternalTestSupport", "_InternalBuildTestSupport"],
24 changes: 24 additions & 0 deletions Sources/SwiftFixIt/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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_library(SwiftFixIt STATIC
SwiftFixIt.swift)
target_link_libraries(SwiftFixIt PUBLIC
Basics

SwiftSyntax::SwiftDiagnostics
SwiftSyntax::SwiftIDEUtils
SwiftSyntax::SwiftParser
SwiftSyntax::SwiftSyntax

TSCBasic
TSCUtility
)

set_target_properties(SwiftFixIt PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
409 changes: 409 additions & 0 deletions Sources/SwiftFixIt/SwiftFixit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,409 @@
//===----------------------------------------------------------------------===//
//
// 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 Basics.AbsolutePath
import protocol Basics.FileSystem
import var Basics.localFileSystem
import struct Basics.SwiftVersion

import struct SwiftDiagnostics.Diagnostic
import struct SwiftDiagnostics.DiagnosticCategory
import protocol SwiftDiagnostics.DiagnosticMessage
import enum SwiftDiagnostics.DiagnosticSeverity
import struct SwiftDiagnostics.DiagnosticsFormatter
import struct SwiftDiagnostics.FixIt
import protocol SwiftDiagnostics.FixItMessage
import struct SwiftDiagnostics.GroupedDiagnostics
import struct SwiftDiagnostics.MessageID

@_spi(FixItApplier)
import enum SwiftIDEUtils.FixItApplier

import struct SwiftParser.Parser

import struct SwiftSyntax.AbsolutePosition
import struct SwiftSyntax.SourceFileSyntax
import class SwiftSyntax.SourceLocationConverter
import struct SwiftSyntax.Syntax

import struct TSCBasic.ByteString
import struct TSCUtility.SerializedDiagnostics

private enum Error: Swift.Error {
case unexpectedDiagnosticSeverity
case failedToResolveSourceLocation
}

// FIXME: An abstraction for tests to work around missing memberwise initializers in `TSCUtility.SerializedDiagnostics`.
protocol AnySourceLocation {
var filename: String { get }
var line: UInt64 { get }
var column: UInt64 { get }
var offset: UInt64 { get }
}

// FIXME: An abstraction for tests to work around missing memberwise initializers in `TSCUtility.SerializedDiagnostics`.
protocol AnyFixIt {
associatedtype SourceLocation: AnySourceLocation

var start: SourceLocation { get }
var end: SourceLocation { get }
var text: String { get }
}

// FIXME: An abstraction for tests to work around missing memberwise initializers in `TSCUtility.SerializedDiagnostics`.
protocol AnyDiagnostic {
associatedtype SourceLocation: AnySourceLocation
associatedtype FixIt: AnyFixIt where FixIt.SourceLocation == SourceLocation

var text: String { get }
var level: SerializedDiagnostics.Diagnostic.Level { get }
var location: SourceLocation? { get }
var category: String? { get }
var categoryURL: String? { get }
var flag: String? { get }
var ranges: [(SourceLocation, SourceLocation)] { get }
var fixIts: [FixIt] { get }
}

extension SerializedDiagnostics.Diagnostic: AnyDiagnostic {}
extension SerializedDiagnostics.SourceLocation: AnySourceLocation {}
extension SerializedDiagnostics.FixIt: AnyFixIt {}

/// The backing API for `SwiftFixitCommand`.
package struct SwiftFixIt /*: ~Copyable */ {
private typealias DiagnosticsPerFile = [SourceFile: [SwiftDiagnostics.Diagnostic]]

private let fileSystem: any FileSystem

private let diagnosticsPerFile: DiagnosticsPerFile

package init(
diagnosticFiles: [AbsolutePath],
fileSystem: any FileSystem
) throws {
// Deserialize the diagnostics.
let diagnostics = try diagnosticFiles.map { path in
let fileContents = try fileSystem.readFileContents(path)
return try TSCUtility.SerializedDiagnostics(bytes: fileContents).diagnostics
}.lazy.joined()

self = try SwiftFixIt(diagnostics: diagnostics, fileSystem: fileSystem)
}

init(
diagnostics: some Collection<some AnyDiagnostic>,
fileSystem: any FileSystem
) throws {
self.fileSystem = fileSystem

// Build a map from source files to `SwiftDiagnostics` diagnostics.
var diagnosticsPerFile: DiagnosticsPerFile = [:]

var diagnosticConverter = DiagnosticConverter(fileSystem: fileSystem)
var currentPrimaryDiagnosticHasNoteWithFixIt = false

for diagnostic in diagnostics {
let hasFixits = !diagnostic.fixIts.isEmpty

if diagnostic.level == .note {
if hasFixits {
// The Swift compiler produces parallel fix-its by attaching
// them to notes, which in turn associate to a single
// error/warning. Prefer the first fix-it in this case: if
// the last error/warning we saw has a note with a fix-it
// and so is this one, skip it.
if currentPrimaryDiagnosticHasNoteWithFixIt {
continue
}

currentPrimaryDiagnosticHasNoteWithFixIt = true
}
} else {
currentPrimaryDiagnosticHasNoteWithFixIt = false
}

// We are only interested in diagnostics with fix-its.
guard hasFixits else {
continue
}

guard let (sourceFile, convertedDiagnostic) =
try diagnosticConverter.diagnostic(from: diagnostic)
else {
continue
}

diagnosticsPerFile[consume sourceFile, default: []].append(consume convertedDiagnostic)
}

self.diagnosticsPerFile = consume diagnosticsPerFile
}

package func applyFixIts() throws {
// Bulk-apply fix-its to each file and write the results back.
for (sourceFile, diagnostics) in self.diagnosticsPerFile {
let result = SwiftIDEUtils.FixItApplier.applyFixes(
from: diagnostics,
filterByMessages: nil,
to: sourceFile.syntax
)

try self.fileSystem.writeFileContents(sourceFile.path, string: consume result)
}
}
}

extension SwiftDiagnostics.DiagnosticSeverity {
fileprivate init?(from level: TSCUtility.SerializedDiagnostics.Diagnostic.Level) {
switch level {
case .ignored:
return nil
case .note:
self = .note
case .warning:
self = .warning
case .error, .fatal:
self = .error
case .remark:
self = .remark
}
}
}

private struct DeserializedDiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
let message: String
let severity: SwiftDiagnostics.DiagnosticSeverity
let category: SwiftDiagnostics.DiagnosticCategory?

var diagnosticID: SwiftDiagnostics.MessageID {
.init(domain: "swift-fixit", id: "\(Self.self)")
}
}

private struct DeserializedFixItMessage: SwiftDiagnostics.FixItMessage {
var message: String { "" }

var fixItID: SwiftDiagnostics.MessageID {
.init(domain: "swift-fixit", id: "\(Self.self)")
}
}

private struct SourceFile {
let path: AbsolutePath
let syntax: SwiftSyntax.SourceFileSyntax

let sourceLocationConverter: SwiftSyntax.SourceLocationConverter

init(path: AbsolutePath, in fileSystem: borrowing some FileSystem) throws {
self.path = path

let bytes = try fileSystem.readFileContents(path)

self.syntax = bytes.contents.withUnsafeBufferPointer { pointer in
SwiftParser.Parser.parse(source: pointer)
}

self.sourceLocationConverter = SwiftSyntax.SourceLocationConverter(
fileName: path.pathString,
tree: self.syntax
)
}

func position(of location: borrowing some AnySourceLocation) throws -> AbsolutePosition {
guard try AbsolutePath(validating: location.filename) == self.path else {
// Wrong source file.
throw Error.failedToResolveSourceLocation
}

guard location.offset == 0 else {
return AbsolutePosition(utf8Offset: Int(location.offset))
}

return self.sourceLocationConverter.position(
ofLine: Int(location.line),
column: Int(location.column)
)
}

func node(at location: some AnySourceLocation) throws -> Syntax {
let position = try position(of: location)

if let token = syntax.token(at: position) {
return SwiftSyntax.Syntax(token)
}

if position == self.syntax.endPosition {
// FIXME: EOF token is not included in '.token(at: position)'
// We might want to include it, but want to avoid special handling.
if let token = syntax.lastToken(viewMode: .all) {
return SwiftSyntax.Syntax(token)
}

return Syntax(self.syntax)
}

// position out of range.
throw Error.failedToResolveSourceLocation
}
}

extension SourceFile: Hashable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.syntax == rhs.syntax
}

func hash(into hasher: inout Hasher) {
hasher.combine(self.syntax)
}
}

private struct DiagnosticConverter /*: ~Copyable */ {
private struct SourceFileCache /*: ~Copyable */ {
private let fileSystem: any FileSystem

private var sourceFiles: [AbsolutePath: SourceFile]

init(fileSystem: any FileSystem) {
self.fileSystem = fileSystem
self.sourceFiles = [:]
}

subscript(location: some AnySourceLocation) -> SourceFile {
mutating get throws {
let path = try AbsolutePath(validating: location.filename)

if let cached = sourceFiles[path] {
return cached
}

let sourceFile = try SourceFile(path: path, in: fileSystem)
sourceFiles[path] = sourceFile

return sourceFile
}
}
}

private var sourceFileCache: SourceFileCache

init(fileSystem: any FileSystem) {
self.sourceFileCache = SourceFileCache(fileSystem: fileSystem)
}
}

extension DiagnosticConverter {
// We expect a fix-it to be in the same source file as the diagnostic it is
// attached to. The opposite can hurt clarity and is more difficult and
// less efficient to model and process in general. The compiler may want to
// actually guard against this pattern and establish a convention to instead
// emit notes with those fix-its.
private static func fixIt(
from diagnostic: borrowing some AnyDiagnostic,
in sourceFile: borrowing SourceFile
) throws -> SwiftDiagnostics.FixIt {
let changes = try diagnostic.fixIts.map { fixIt in
let startPosition = try sourceFile.position(of: fixIt.start)
let endPosition = try sourceFile.position(of: fixIt.end)

return SwiftDiagnostics.FixIt.Change.replaceText(
range: startPosition ..< endPosition,
with: fixIt.text,
in: Syntax(sourceFile.syntax)
)
}

return SwiftDiagnostics.FixIt(message: DeserializedFixItMessage(), changes: changes)
}

private static func highlights(
from diagnostic: borrowing some AnyDiagnostic,
in sourceFile: borrowing SourceFile
) throws -> [Syntax] {
try diagnostic.ranges.map { startLocation, endLocation in
let startPosition = try sourceFile.position(of: startLocation)
let endPosition = try sourceFile.position(of: endLocation)

var highlightedNode = try sourceFile.node(at: startLocation)

// Walk up from the start token until we find a syntax node that matches
// the highlight range.
while true {
// If this syntax matches our starting/ending positions, add the
// highlight and we're done.
if highlightedNode.positionAfterSkippingLeadingTrivia == startPosition
&& highlightedNode.endPositionBeforeTrailingTrivia == endPosition
{
break
}

// Go up to the parent.
guard let parent = highlightedNode.parent else {
break
}

highlightedNode = parent
}

return highlightedNode
}
}

typealias Diagnostic = (sourceFile: SourceFile, diagnostic: SwiftDiagnostics.Diagnostic)

mutating func diagnostic(
from diagnostic: borrowing some AnyDiagnostic
) throws -> Diagnostic? {
if diagnostic.fixIts.isEmpty {
preconditionFailure("Expected diagnostic with fix-its")
}

guard let location = diagnostic.location else {
return nil
}

let message: DeserializedDiagnosticMessage
do {
guard let severity = SwiftDiagnostics.DiagnosticSeverity(from: diagnostic.level) else {
return nil
}

let category: SwiftDiagnostics.DiagnosticCategory? =
if let category = diagnostic.category {
.init(name: category, documentationURL: diagnostic.categoryURL)
} else {
nil
}

message = .init(
message: diagnostic.text,
severity: severity,
category: category
)
}

let sourceFile = try sourceFileCache[location]

return try Diagnostic(
sourceFile: sourceFile,
diagnostic: SwiftDiagnostics.Diagnostic(
node: sourceFile.node(at: location),
position: sourceFile.position(of: location),
message: message,
highlights: Self.highlights(from: diagnostic, in: sourceFile),
fixIts: [
Self.fixIt(from: diagnostic, in: sourceFile),
]
)
)
}
}
609 changes: 609 additions & 0 deletions Tests/SwiftFixItTests/SwiftFixItTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,609 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftFixIt
import struct TSCUtility.SerializedDiagnostics
import XCTest

final class SwiftFixItTests: XCTestCase {
private struct TestDiagnostic: AnyDiagnostic {
struct SourceLocation: AnySourceLocation {
let filename: String
let line: UInt64
let column: UInt64
let offset: UInt64
}

struct FixIt: AnyFixIt {
let start: TestDiagnostic.SourceLocation
let end: TestDiagnostic.SourceLocation
let text: String
}

var text: String
var level: SerializedDiagnostics.Diagnostic.Level
var location: SourceLocation?
var category: String?
var categoryURL: String?
var flag: String?
var ranges: [(SourceLocation, SourceLocation)] = []
var fixIts: [FixIt] = []
}

private struct SourceFileEdit {
let input: String
let result: String
}

private struct TestCase<T> {
let edits: T
let diagnostics: [TestDiagnostic]
}

private func _testAPI(
_ sourceFilePathsAndEdits: [(AbsolutePath, SourceFileEdit)],
_ diagnostics: [TestDiagnostic]
) throws {
for (path, edit) in sourceFilePathsAndEdits {
try localFileSystem.writeFileContents(path, string: edit.input)
}

let swiftFixIt = try SwiftFixIt(diagnostics: diagnostics, fileSystem: localFileSystem)
try swiftFixIt.applyFixIts()

for (path, edit) in sourceFilePathsAndEdits {
try XCTAssertEqual(localFileSystem.readFileContents(path), edit.result)
}
}

private func uniqueSwiftFileName() -> String {
"\(UUID().uuidString).swift"
}

// Cannot use variadic generics: crashes.
private func testAPI1File(
_ getTestCase: (String) -> TestCase<SourceFileEdit>
) throws {
try testWithTemporaryDirectory { fixturePath in
let sourceFilePath = fixturePath.appending(self.uniqueSwiftFileName())

let testCase = getTestCase(sourceFilePath.pathString)

try self._testAPI(
[(sourceFilePath, testCase.edits)],
testCase.diagnostics
)
}
}

private func testAPI2Files(
_ getTestCase: (String, String) -> TestCase<(SourceFileEdit, SourceFileEdit)>
) throws {
try testWithTemporaryDirectory { fixturePath in
let sourceFilePath1 = fixturePath.appending(self.uniqueSwiftFileName())
let sourceFilePath2 = fixturePath.appending(self.uniqueSwiftFileName())

let testCase = getTestCase(sourceFilePath1.pathString, sourceFilePath2.pathString)

try self._testAPI(
[(sourceFilePath1, testCase.edits.0), (sourceFilePath2, testCase.edits.1)],
testCase.diagnostics
)
}
}
}

extension SwiftFixItTests {
func testFixIt() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "let x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testFixItIgnoredDiagnostic() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "var x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .ignored,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Ignore, diagnostic is ignored.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testFixItNoLocation() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "var x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: nil,
fixIts: [
// Ignore, diagnostic without location.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testNoteFixIt() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "let x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testNonParallelNoteFixIts() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "let x = 22"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
// Make sure we apply this fix-it.
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename, line: 1, column: 9, offset: 0),
end: .init(filename: filename, line: 1, column: 10, offset: 0),
text: "22"
),
]
),
]
)
}
}

func testFixItsOnDifferentLines() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(
input: """
var x = 1
var y = 2
var z = 3
""",
result: """
let x = 1
var y = 244
z = 3
"""
),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Replacement.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
// Addition.
.init(
start: .init(filename: filename, line: 2, column: 10, offset: 0),
end: .init(filename: filename, line: 2, column: 10, offset: 0),
text: "44"
),
// Deletion.
.init(
start: .init(filename: filename, line: 3, column: 1, offset: 0),
end: .init(filename: filename, line: 3, column: 5, offset: 0),
text: ""
),
]
),
]
)
}
}

func testNonOverlappingFixItsOnSameLine() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = foo(1, 2)", result: "x = fooo(1, 233)"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Replacement.
.init(
start: .init(filename: filename, line: 1, column: 9, offset: 0),
end: .init(filename: filename, line: 1, column: 12, offset: 0),
text: "fooo"
),
// Addition.
.init(
start: .init(filename: filename, line: 1, column: 17, offset: 0),
end: .init(filename: filename, line: 1, column: 17, offset: 0),
text: "33"
),
// Deletion.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 5, offset: 0),
text: ""
),
]
),
]
)
}
}

func testOverlappingFixItsSingleDiagnostic() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "_ = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Applied.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 6, offset: 0),
text: "_"
),
// Ignored, overlaps with previous fix-it.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testOverlappingFixItsDifferentDiagnostics() throws {
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "_ = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Applied.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 6, offset: 0),
text: "_"
),
]
),
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Ignored, overlaps with previous fix-it.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testParallelFixIts1() throws {
// First parallel fix-it is applied per emission order.
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "let x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Applied.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Ignored, parallel to previous fix-it.
.init(
start: .init(filename: filename, line: 1, column: 9, offset: 0),
end: .init(filename: filename, line: 1, column: 10, offset: 0),
text: "22"
),
]
),
]
)
}
}

func testParallelFixIts2() throws {
// First parallel fix-it is applied per emission order.
try self.testAPI1File { (filename: String) in
.init(
edits: .init(input: "var x = 1", result: "let x = 1"),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Applied.
.init(
start: .init(filename: filename, line: 1, column: 1, offset: 0),
end: .init(filename: filename, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename, line: 1, column: 1, offset: 0),
fixIts: [
// Ignored, parallel to previous fix-it.
.init(
start: .init(filename: filename, line: 1, column: 9, offset: 0),
end: .init(filename: filename, line: 1, column: 10, offset: 0),
text: "22"
),
]
),
]
)
}
}

func testFixItsMultipleFiles() throws {
try self.testAPI2Files { (filename1: String, filename2: String) in
.init(
edits: (
.init(input: "var x = 1", result: "let x = 1"),
.init(input: "var x = 1", result: "let x = 1")
),
diagnostics: [
// filename1
TestDiagnostic(
text: "warning",
level: .warning,
location: .init(filename: filename1, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename1, line: 1, column: 1, offset: 0),
end: .init(filename: filename1, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename1, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename1, line: 1, column: 1, offset: 0),
end: .init(filename: filename1, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
// filename2
TestDiagnostic(
text: "warning",
level: .warning,
location: .init(filename: filename2, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename2, line: 1, column: 1, offset: 0),
end: .init(filename: filename2, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename2, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename2, line: 1, column: 1, offset: 0),
end: .init(filename: filename2, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testFixItNoteInDifferentFile() throws {
try self.testAPI2Files { (filename1: String, filename2: String) in
.init(
edits: (
.init(input: "var x = 1", result: "let x = 1"),
.init(input: "var x = 1", result: "var x = 1")
),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename2, line: 1, column: 1, offset: 0)
),
TestDiagnostic(
text: "note",
level: .note,
location: .init(filename: filename1, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename1, line: 1, column: 1, offset: 0),
end: .init(filename: filename1, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
}

func testFixItInDifferentFile() {
do {
try self.testAPI2Files { (filename1: String, filename2: String) in
.init(
edits: (
.init(input: "var x = 1", result: "let x = 1"),
.init(input: "", result: "")
),
diagnostics: [
TestDiagnostic(
text: "error",
level: .error,
location: .init(filename: filename2, line: 1, column: 1, offset: 0),
fixIts: [
.init(
start: .init(filename: filename1, line: 1, column: 1, offset: 0),
end: .init(filename: filename1, line: 1, column: 4, offset: 0),
text: "let"
),
]
),
]
)
}
} catch {
// Expected to throw an error.
return
}

XCTFail()
}
}