Skip to content

Eliminate Foundation dependency from SwiftSyntaxBuilder #1015

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

Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,57 @@
//===----------------------------------------------------------------------===//

import SwiftSyntax
import Foundation

/// A regular expression matching sequences of `#`s with an adjacent quote or
/// interpolation. Used to determine the number of `#`s for a raw string literal.
private let rawStringPotentialEscapesPattern = try! NSRegularExpression(
pattern: [
#""(#*)"#, // Match potential opening delimiters
#"(#*)""#, // Match potential closing delimiters
#"\\(#*)"#, // Match potential interpolations
].joined(separator: "|")
)

/// Count the number of # signs before/after a `#` or between a `\` and a `(`.
private func countPounds(_ string: String) -> Int {
var afterQuote = false
var afterBackslash = false

var consecutivePounds = 0

var maxPounds = 0

func updateForAnyCharacter() {
if afterBackslash {
maxPounds = max(maxPounds, consecutivePounds + 1)
}

if afterQuote {
maxPounds = max(maxPounds, consecutivePounds + 1)
}
}

for c in string {
switch c {
case #"""#:
maxPounds = max(maxPounds, consecutivePounds + 1)
afterQuote = true
afterBackslash = false
consecutivePounds = 0

case #"\"#:
if afterQuote {
maxPounds = max(maxPounds, consecutivePounds + 1)
afterQuote = false
}
afterBackslash = true
consecutivePounds = 0

case "#":
consecutivePounds += 1

default:
updateForAnyCharacter()
afterQuote = false
afterBackslash = false
consecutivePounds = 0
}
}

updateForAnyCharacter()

return maxPounds
}

extension StringLiteralExpr {
/// Creates a string literal, optionally specifying quotes and delimiters.
Expand All @@ -41,13 +81,7 @@ extension StringLiteralExpr {
var openDelimiter = openDelimiter
var closeDelimiter = closeDelimiter
if openDelimiter == nil, closeDelimiter == nil {
// Match potential escapes in the string
let matches = rawStringPotentialEscapesPattern.matches(in: content, range: NSRange(content.startIndex..., in: content))

// Find longest sequence of `#`s by taking the maximum length over all captures
let poundCount = matches
.compactMap { match in (1..<match.numberOfRanges).map { match.range(at: $0).length + 1 }.max() }
.max() ?? 0
let poundCount = countPounds(content)

// Use a delimiter that is exactly one longer
openDelimiter = Token.rawStringDelimiter(String(repeating: "#", count: poundCount))
Expand Down
14 changes: 14 additions & 0 deletions Tests/SwiftSyntaxBuilderTest/StringLiteralTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,23 @@ final class StringLiteralTests: XCTestCase {
}
}

func testEscapeLiteral() {
AssertBuildResult(
StringLiteralExpr(content: #""""foobar""#),
##"#""""foobar""#"##
)
}

func testEscapeBackslash() {
AssertBuildResult(StringLiteralExpr(content: #"\"#), ##"""
#"\"#
"""##)
}

func testEscapePounds() {
AssertBuildResult(
StringLiteralExpr(content: ###""foobar"##foobar"###),
#####"###""foobar"##foobar"###"#####
)
}
}