Skip to content

Introduce FixIt.Change.replaceText #3030

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 3 commits into from
Mar 29, 2025
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
18 changes: 13 additions & 5 deletions Release Notes/602.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

## New APIs

- `DiagnosticMessage` has a new optional property, `category`, that providesa category name and documentation URL for a diagnostic.
- Description: Tools often have many different diagnostics. Diagnostic categories allow tools to group several diagnostics together with documentation that can help users understand what the diagnostics mean and how to address them. This API allows diagnostics to provide this category information. The diagnostic renderer will provide the category at the end of the diagnostic message in the form `[#CategoryName]`, and can print categories as "footnotes" with its `categoryFootnotes` method.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2981
- Migration steps: None required. The new `category` property has optional type, and there is a default implementation that returns `nil`. Types that conform to `DiagnosticMessage` can choose to implement this property and provide a category when appropriate.

- `SwiftLexicalLookup` - A new Swift unqualified lookup library
- Description: The library provides a new Swift unqualified lookup implementation detached from the compiler and accessible to outside clients. The query is stateless and can be directly run on swift-syntax syntax tree, with any syntax node functioning as an entry point. It produces an enum-based data structure as a result that partitions collected names based on the lexical scope of introduction.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2952
- Notes: The library follows the behavior of the compiler implementation with some minor differences, such as a different way of handling dollar identifiers `$x` and generic parameters inside extensions. Furthermore, in the future, once the compiler adopts `SwiftLexicalLookup` and it becomes the canonical implementation, results produced by the query will be guaranteed to be correct.

- `DiagnosticMessage` has a new optional property, `category`, that provides a category name and documentation URL for a diagnostic.
- Description: Tools often have many different diagnostics. Diagnostic categories allow tools to group several diagnostics together with documentation that can help users understand what the diagnostics mean and how to address them. This API allows diagnostics to provide this category information. The diagnostic renderer will provide the category at the end of the diagnostic message in the form `[#CategoryName]`, and can print categories as "footnotes" with its `categoryFootnotes` method.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2981
- Migration steps: None required. The new `category` property has optional type, and there is a default implementation that returns `nil`. Types that conform to `DiagnosticMessage` can choose to implement this property and provide a category when appropriate.

- `FixIt.Change` has a new case `replaceText` that performs a textual replacement of a range of text with another string.
- Description: The other `FixIt.Change` cases provide structured
modifications to syntax trees, such as replacing specific notes. Some
clients provide Fix-Its that don't fit well into this structured model. The
`replaceText` case makes it possible for such clients to express Fix-Its.
- Pull request: https://github.com/swiftlang/swift-syntax/pull/3030
- Migration stems: None required.

## API Behavior Changes

## Deprecations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ extension PluginMessage.Diagnostic {
case .replaceChild(let replaceChildData):
range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent)
text = replaceChildData.newChild.description
case .replaceText(
range: let replacementRange,
with: let newText,
in: let syntax
):
range = sourceManager.range(replacementRange, in: syntax)
text = newText
#if RESILIENT_LIBRARIES
@unknown default:
fatalError()
Expand Down
11 changes: 11 additions & 0 deletions Sources/SwiftDiagnostics/FixIt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ public struct FixIt: Sendable {
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
/// Replace the child node of the given parent node at the given replacement range with the given new child node
case replaceChild(data: any ReplacingChildData)
/// Replace the text within the given range in a syntax node with new text.
///
/// Generally, one should use other cases to replace specific syntax nodes
/// or trivia, because it more easily leads to a correct result. However,
/// this case provides a fallback for textual replacement that ignores
/// syntactic structure. After applying a textual replacement, there is no
/// way to get back to a syntax tree without reparsing.
case replaceText(range: Range<AbsolutePosition>, with: String, in: Syntax)
}

/// A description of what this Fix-It performs.
Expand Down Expand Up @@ -157,6 +165,9 @@ private extension FixIt.Change {
range: replacingChildData.replacementRange,
replacement: replacingChildData.newChild.description
)

case .replaceText(range: let replacementRange, with: let newText, in: _):
return SourceEdit(range: replacementRange, replacement: newText)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,11 @@ fileprivate extension FixIt.Change {
range: start..<end,
replacement: replacingChildData.newChild.description
)

case .replaceText(range: let range, with: let newText, in: let syntax):
let start = expansionContext.position(of: range.lowerBound, anchoredAt: syntax)
let end = expansionContext.position(of: range.upperBound, anchoredAt: syntax)
return SourceEdit(range: start..<end, replacement: newText)
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions Tests/SwiftDiagnosticsTest/FixItTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@
//
//===----------------------------------------------------------------------===//

import SwiftDiagnostics
import SwiftParser
import SwiftParserDiagnostics
import SwiftSyntax
import XCTest
import _SwiftSyntaxTestSupport

struct SimpleFixItMessage: FixItMessage {
let message: String

var fixItID: MessageID {
MessageID(domain: "here", id: "this")
}
}

final class FixItTests: XCTestCase {
func testEditsForFixIt() throws {
let markedSource = "protocol 1️⃣Multi2️⃣ 3️⃣ident 4️⃣{}"
Expand All @@ -39,4 +48,22 @@ final class FixItTests: XCTestCase {
XCTAssertNotEqual(changes.count, edits.count)
XCTAssertEqual(expectedEdits, edits)
}

func testTextualReplacement() throws {
let five = AbsolutePosition(utf8Offset: 5)
let fifteen = AbsolutePosition(utf8Offset: 15)
let change = FixIt(
message: SimpleFixItMessage(message: "fix it please"),
changes: [
.replaceText(
range: five..<fifteen,
with: "yours",
in: Syntax("func myFunction() { }" as SourceFileSyntax)
)
]
)
XCTAssertEqual(change.edits.count, 1)
XCTAssertEqual(change.edits[0].range, five..<fifteen)
XCTAssertEqual(change.edits[0].replacement, "yours")
}
}