From 4003c14e8f3d87aea63066627c1f47a855731259 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 27 Mar 2025 17:34:38 -0700 Subject: [PATCH 1/3] Introduce FixIt.Change.textualReplacement Introduce a new case to FixIt.Change to express an unstructured edit, which replaces some range of source text (in a given file) with some other source text. This is needed for some edits that aren't easily mapped to the syntax tree, or when coming from other tools (such as the compiler) that don't express these fixes in terms of syntax in the first place. --- .../Diagnostics.swift | 3 +++ Sources/SwiftDiagnostics/FixIt.swift | 11 +++++++++ .../Assertions.swift | 5 ++++ Tests/SwiftDiagnosticsTest/FixItTests.swift | 23 +++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift index 33a7425fcf0..120f48338c0 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift @@ -150,6 +150,9 @@ extension PluginMessage.Diagnostic { case .replaceChild(let replaceChildData): range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent) text = replaceChildData.newChild.description + case .textualReplacement(replacementRange: let replacementRange, sourceFile: let sourceFile, newText: let newText): + range = sourceManager.range(replacementRange, in: sourceFile) + text = newText #if RESILIENT_LIBRARIES @unknown default: fatalError() diff --git a/Sources/SwiftDiagnostics/FixIt.swift b/Sources/SwiftDiagnostics/FixIt.swift index d7a25baeaa8..4901047507b 100644 --- a/Sources/SwiftDiagnostics/FixIt.swift +++ b/Sources/SwiftDiagnostics/FixIt.swift @@ -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 source file 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 textualReplacement(replacementRange: Range, sourceFile: SourceFileSyntax, newText: String) } /// A description of what this Fix-It performs. @@ -157,6 +165,9 @@ private extension FixIt.Change { range: replacingChildData.replacementRange, replacement: replacingChildData.newChild.description ) + + case .textualReplacement(replacementRange: let replacementRange, sourceFile: _, newText: let newText): + return SourceEdit(range: replacementRange, replacement: newText) } } } diff --git a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift index d1e96499ee0..56ccf149354 100644 --- a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift @@ -632,6 +632,11 @@ fileprivate extension FixIt.Change { range: start.. Date: Thu, 27 Mar 2025 17:43:33 -0700 Subject: [PATCH 2/3] Formatting --- .../SwiftCompilerPluginMessageHandling/Diagnostics.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift index 120f48338c0..a5dab223917 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift @@ -150,7 +150,11 @@ extension PluginMessage.Diagnostic { case .replaceChild(let replaceChildData): range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent) text = replaceChildData.newChild.description - case .textualReplacement(replacementRange: let replacementRange, sourceFile: let sourceFile, newText: let newText): + case .textualReplacement( + replacementRange: let replacementRange, + sourceFile: let sourceFile, + newText: let newText + ): range = sourceManager.range(replacementRange, in: sourceFile) text = newText #if RESILIENT_LIBRARIES From 664e88179f1c8fa40cf15185fc12484147f2cee3 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 28 Mar 2025 14:28:05 -0700 Subject: [PATCH 3/3] Rename FixIt.Change.textualReplacement to replaceText(range:with:in:) Rename according to the discussion. Additionally, only require a Syntax node (not a SourceFileSyntax) as the place we're rewriting from. --- Release Notes/602.md | 18 +++++++++++++----- .../Diagnostics.swift | 10 +++++----- Sources/SwiftDiagnostics/FixIt.swift | 6 +++--- .../Assertions.swift | 6 +++--- Tests/SwiftDiagnosticsTest/FixItTests.swift | 6 +++++- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Release Notes/602.md b/Release Notes/602.md index ffc24f54a83..fe25fbf52db 100644 --- a/Release Notes/602.md +++ b/Release Notes/602.md @@ -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 diff --git a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift index a5dab223917..277212cd08c 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift @@ -150,12 +150,12 @@ extension PluginMessage.Diagnostic { case .replaceChild(let replaceChildData): range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent) text = replaceChildData.newChild.description - case .textualReplacement( - replacementRange: let replacementRange, - sourceFile: let sourceFile, - newText: let newText + case .replaceText( + range: let replacementRange, + with: let newText, + in: let syntax ): - range = sourceManager.range(replacementRange, in: sourceFile) + range = sourceManager.range(replacementRange, in: syntax) text = newText #if RESILIENT_LIBRARIES @unknown default: diff --git a/Sources/SwiftDiagnostics/FixIt.swift b/Sources/SwiftDiagnostics/FixIt.swift index 4901047507b..af238a31c6c 100644 --- a/Sources/SwiftDiagnostics/FixIt.swift +++ b/Sources/SwiftDiagnostics/FixIt.swift @@ -77,14 +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 source file with new text. + /// 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 textualReplacement(replacementRange: Range, sourceFile: SourceFileSyntax, newText: String) + case replaceText(range: Range, with: String, in: Syntax) } /// A description of what this Fix-It performs. @@ -166,7 +166,7 @@ private extension FixIt.Change { replacement: replacingChildData.newChild.description ) - case .textualReplacement(replacementRange: let replacementRange, sourceFile: _, newText: let newText): + case .replaceText(range: let replacementRange, with: let newText, in: _): return SourceEdit(range: replacementRange, replacement: newText) } } diff --git a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift index 56ccf149354..7d7e490cbc0 100644 --- a/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift @@ -633,9 +633,9 @@ fileprivate extension FixIt.Change { replacement: replacingChildData.newChild.description ) - case .textualReplacement(replacementRange: let range, sourceFile: let sourceFile, newText: let newText): - let start = expansionContext.position(of: range.lowerBound, anchoredAt: sourceFile) - let end = expansionContext.position(of: range.upperBound, anchoredAt: sourceFile) + 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..