Skip to content

Commit fa136a9

Browse files
committed
Add convenience initializer for raw string literals
Automatically determine the number of #s required to express the string without escapes.
1 parent f653d62 commit fa136a9

File tree

2 files changed

+53
-4
lines changed

2 files changed

+53
-4
lines changed

Sources/SwiftSyntaxBuilder/StringLiteralExprConvenienceInitializers.swift

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,58 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import SwiftSyntax
14+
import Foundation
15+
16+
/// A regular expression matching sequences of `#`s with an adjacent quote or
17+
/// interpolation. Used to determine the number of `#`s for a raw string literal.
18+
private let rawStringPotentialEscapesPattern = try! NSRegularExpression(
19+
pattern: [
20+
#""(#+)"#, // Match potential opening delimiters
21+
#"(#+)""#, // Match potential closing delimiters
22+
#"\\(#+)\("#, // Match potential interpolations
23+
].joined(separator: "|")
24+
)
1425

1526
extension StringLiteralExpr {
16-
public init(_ value: String, openQuote: TokenSyntax = .stringQuote, closeQuote: TokenSyntax = .stringQuote) {
27+
/// Creates a string literal, optionally specifying quotes and delimiters.
28+
public init(
29+
openDelimiter: TokenSyntax? = nil,
30+
openQuote: TokenSyntax = .stringQuote,
31+
_ value: String,
32+
closeQuote: TokenSyntax = .stringQuote,
33+
closeDelimiter: TokenSyntax? = nil
34+
) {
1735
let content = TokenSyntax.stringSegment(value)
1836
let segment = StringSegment(content: content)
1937
let segments = StringLiteralSegments([segment])
2038

21-
self.init(openQuote: openQuote,
22-
segments: segments,
23-
closeQuote: closeQuote)
39+
self.init(
40+
openDelimiter: openDelimiter,
41+
openQuote: openQuote,
42+
segments: segments,
43+
closeQuote: closeQuote,
44+
closeDelimiter: closeDelimiter
45+
)
46+
}
47+
48+
/// Creates a raw string literal. Automatically determines
49+
/// the number of `#`s needed to express the string as-is without any escapes.
50+
public init(raw value: String) {
51+
// Match potential escapes in the string
52+
let matches = rawStringPotentialEscapesPattern.matches(in: value, range: NSRange(value.startIndex..., in: value))
53+
54+
// Find longest sequence of `#`s by taking the maximum length over all captures
55+
let maxPoundCount = matches
56+
.compactMap { match in (1..<match.numberOfRanges).map { match.range(at: $0).length }.max() }
57+
.max() ?? 0
58+
59+
// Use a delimiter that is exactly one longer
60+
let delimiter = TokenSyntax.rawStringDelimiter(String(repeating: "#", count: 1 + maxPoundCount))
61+
62+
self.init(
63+
openDelimiter: delimiter,
64+
value,
65+
closeDelimiter: delimiter
66+
)
2467
}
2568
}

Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ final class StringLiteralTests: XCTestCase {
3333
#line: (StringLiteralExpr("asdf"), #"␣"asdf""#),
3434
#line: ("", #"␣"""#),
3535
#line: ("asdf", #"␣"asdf""#),
36+
#line: (StringLiteralExpr(raw: "abc"), "␣#\"abc\"#"),
37+
#line: (StringLiteralExpr(raw: #""quoted""#), ##"␣#""quoted""#"##),
38+
#line: (StringLiteralExpr(raw: ##"#"rawquoted"#"##), ###"␣##"#"rawquoted"#"##"###),
39+
#line: (StringLiteralExpr(raw: ####"###"unbalanced"####), #####"␣####"###"unbalanced"####"#####),
40+
#line: (StringLiteralExpr(raw: ###"some "# string ##""###), ####"␣###"some "# string ##""###"####),
41+
#line: (StringLiteralExpr(raw: ###"\##(abc) \(def)"###), ####"␣###"\##(abc) \(def)"###"####),
3642
]
3743

3844
for (line, testCase) in testCases {

0 commit comments

Comments
 (0)