Skip to content

Commit 990b24d

Browse files
authored
Merge pull request #1016 from bnbarham/string-literal-no-regex
Remove Foundation dependency in SwiftSyntaxBuilder
2 parents 6da4d9f + 37595c1 commit 990b24d

File tree

2 files changed

+87
-23
lines changed

2 files changed

+87
-23
lines changed

Sources/SwiftSyntaxBuilder/ConvenienceInitializers/StringLiteralExprConvenienceInitializers.swift

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,6 @@
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-
)
2514

2615
extension StringLiteralExpr {
2716
/// Creates a string literal, optionally specifying quotes and delimiters.
@@ -41,17 +30,12 @@ extension StringLiteralExpr {
4130
var openDelimiter = openDelimiter
4231
var closeDelimiter = closeDelimiter
4332
if openDelimiter == nil, closeDelimiter == nil {
44-
// Match potential escapes in the string
45-
let matches = rawStringPotentialEscapesPattern.matches(in: content, range: NSRange(content.startIndex..., in: content))
46-
47-
// Find longest sequence of `#`s by taking the maximum length over all captures
48-
let poundCount = matches
49-
.compactMap { match in (1..<match.numberOfRanges).map { match.range(at: $0).length + 1 }.max() }
50-
.max() ?? 0
51-
52-
// Use a delimiter that is exactly one longer
53-
openDelimiter = Token.rawStringDelimiter(String(repeating: "#", count: poundCount))
54-
closeDelimiter = openDelimiter
33+
let (requiresEscaping, poundCount) = requiresEscaping(content)
34+
if requiresEscaping {
35+
// Use a delimiter that is exactly one longer
36+
openDelimiter = Token.rawStringDelimiter(String(repeating: "#", count: poundCount + 1))
37+
closeDelimiter = openDelimiter
38+
}
5539
}
5640

5741
self.init(
@@ -63,3 +47,41 @@ extension StringLiteralExpr {
6347
)
6448
}
6549
}
50+
51+
private enum PoundState {
52+
case afterQuote, afterBackslash, none
53+
}
54+
55+
private func requiresEscaping(_ content: String) -> (Bool, poundCount: Int) {
56+
var state: PoundState = .none
57+
var consecutivePounds = 0
58+
var maxPounds = 0
59+
var requiresEscaping = false
60+
61+
for c in content {
62+
switch c {
63+
case "#":
64+
consecutivePounds += 1
65+
case "\"":
66+
state = .afterQuote
67+
consecutivePounds = 0
68+
case "\\":
69+
state = .afterBackslash
70+
consecutivePounds = 0
71+
case "(" where state == .afterBackslash:
72+
maxPounds = max(maxPounds, consecutivePounds)
73+
fallthrough
74+
default:
75+
consecutivePounds = 0
76+
state = .none
77+
}
78+
79+
if state == .afterQuote {
80+
maxPounds = max(maxPounds, consecutivePounds)
81+
}
82+
83+
requiresEscaping = requiresEscaping || state != .none
84+
}
85+
86+
return (requiresEscaping, poundCount: maxPounds)
87+
}

Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,50 @@ final class StringLiteralTests: XCTestCase {
3535
}
3636
}
3737

38+
func testRegular() {
39+
AssertBuildResult(
40+
StringLiteralExpr(content: "foobar"),
41+
"""
42+
"foobar"
43+
"""
44+
)
45+
46+
AssertBuildResult(
47+
StringLiteralExpr(content: "##foobar"),
48+
"""
49+
"##foobar"
50+
"""
51+
)
52+
}
53+
54+
func testEscapeLiteral() {
55+
AssertBuildResult(
56+
StringLiteralExpr(content: #""""foobar""#),
57+
##"""
58+
#""""foobar""#
59+
"""##
60+
)
61+
}
62+
63+
func testEscapePounds() {
64+
AssertBuildResult(
65+
StringLiteralExpr(content: ###"#####"foobar"##foobar"#foobar"###),
66+
#####"""
67+
###"#####"foobar"##foobar"#foobar"###
68+
"""#####
69+
)
70+
}
71+
72+
func testEscapeInteropolation() {
73+
AssertBuildResult(StringLiteralExpr(content: ###"\##(foobar)\#(foobar)"###),
74+
####"""
75+
###"\##(foobar)\#(foobar)"###
76+
"""####)
77+
}
78+
3879
func testEscapeBackslash() {
39-
AssertBuildResult(StringLiteralExpr(content: #"\"#), ##"""
80+
AssertBuildResult(StringLiteralExpr(content: #"\"#),
81+
##"""
4082
#"\"#
4183
"""##)
4284
}

0 commit comments

Comments
 (0)