From 755dedb7dd0be4d5717d774d8982be5595f753ea Mon Sep 17 00:00:00 2001 From: Andrew Devasia Date: Sun, 1 Feb 2026 15:03:40 +0530 Subject: [PATCH] Ensure FormatRawStringLiteral returns valid swift syntax --- .../FormatRawStringLiteral.swift | 20 ++++++------------- .../FormatRawStringLiteral.swift | 9 ++++++--- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/Sources/SwiftRefactor/FormatRawStringLiteral.swift b/Sources/SwiftRefactor/FormatRawStringLiteral.swift index c8208b56ec3..366e402a0ff 100644 --- a/Sources/SwiftRefactor/FormatRawStringLiteral.swift +++ b/Sources/SwiftRefactor/FormatRawStringLiteral.swift @@ -32,18 +32,16 @@ import SwiftSyntax /// ```swift /// ##"The # of values is \(count)"## /// ##"Hello \#(world)"## -/// "Hello World" +/// #"Hello World"# /// ``` public struct FormatRawStringLiteral: SyntaxRefactoringProvider { public static func refactor(syntax lit: StringLiteralExprSyntax, in context: Void) -> StringLiteralExprSyntax { var maximumHashes = 0 for segment in lit.segments { switch segment { - case .expressionSegment(let expr): - if let rawStringDelimiter = expr.pounds { - // Pick up any delimiters in interpolation segments \#...#(...) - maximumHashes = max(maximumHashes, rawStringDelimiter.text.longestRun(of: "#")) - } + case .expressionSegment: + // Valid string interpolations require matching the delimiter count + return lit case .stringSegment(let string): // Find the longest run of # characters in the content of the literal. maximumHashes = max(maximumHashes, string.content.text.longestRun(of: "#")) @@ -54,14 +52,8 @@ public struct FormatRawStringLiteral: SyntaxRefactoringProvider { } } - guard maximumHashes > 0 else { - return - lit - .with(\.openingPounds, lit.openingPounds?.with(\.tokenKind, .rawStringPoundDelimiter(""))) - .with(\.closingPounds, lit.closingPounds?.with(\.tokenKind, .rawStringPoundDelimiter(""))) - } - - let delimiters = String(repeating: "#", count: maximumHashes + 1) + // Ensure at least one delimiter for raw string literals + let delimiters = String(repeating: "#", count: max(1, maximumHashes + 1)) return lit .with(\.openingPounds, lit.openingPounds?.with(\.tokenKind, .rawStringPoundDelimiter(delimiters))) diff --git a/Tests/SwiftRefactorTest/FormatRawStringLiteral.swift b/Tests/SwiftRefactorTest/FormatRawStringLiteral.swift index 59d5f3e8012..053c077afc5 100644 --- a/Tests/SwiftRefactorTest/FormatRawStringLiteral.swift +++ b/Tests/SwiftRefactorTest/FormatRawStringLiteral.swift @@ -21,8 +21,7 @@ final class FormatRawStringLiteralTest: XCTestCase { func testDelimiterPlacement() throws { let tests = [ (#line, literal: #" "Hello World" "#, expectation: #" "Hello World" "#), - (#line, literal: ##" #"Hello World" "##, expectation: #" "Hello World" "#), - (#line, literal: ##" #"Hello World"# "##, expectation: #" "Hello World" "#), + (#line, literal: ##" #"Hello World"# "##, expectation: ##" #"Hello World"# "##), (#line, literal: #####" "####" "#####, expectation: #####" "####" "#####), (#line, literal: #####" #"####"# "#####, expectation: ######" #####"####"##### "######), (#line, literal: #####" #"\####(hello)"# "#####, expectation: ######" #####"\####(hello)"##### "######), @@ -30,7 +29,11 @@ final class FormatRawStringLiteralTest: XCTestCase { #line, literal: #######" #"###### \####(hello) ##"# "#######, expectation: ########" #######"###### \####(hello) ##"####### "######## ), - (#line, literal: ########" #######"hello \(world) "####### "########, expectation: #" "hello \(world) " "#), + (#line, literal: ########" #######"hello \(world) "####### "########, expectation: ##" #"hello \(world) "# "##), + (#line, literal: ###" ##"""## "###, expectation: ##" #"""# "##), + (#line, literal: ####" ###"C:\Users\swift"### "####, expectation: ##" #"C:\Users\swift"# "##), + (#line, literal: #####" ####"He said "Hi""#### "#####, expectation: ##" #"He said "Hi""# "##), + (#line, literal: #####" ###"Value: \###(count)"### "#####, expectation: ####" ###"Value: \###(count)"### "####), ] for (line, literal, expectation) in tests {