Skip to content

Commit 9474729

Browse files
authored
Merge pull request #1083 from CodaFi/hash-bang
Add Refactoring Action to Format Raw String Delimiters
2 parents bab6bb2 + 26a5299 commit 9474729

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Format a string literal by inserting or removing the appropriate number of
16+
/// raw string `#` delimiters.
17+
///
18+
/// ## Before
19+
///
20+
/// ```swift
21+
/// "The # of values is \(count)"
22+
/// "Hello \#(world)"
23+
/// ###"Hello World"###
24+
/// ```
25+
///
26+
/// ## After
27+
///
28+
/// ```swift
29+
/// ##"The # of values is \(count)"##
30+
/// ##"Hello \#(world)"##
31+
/// "Hello World"
32+
/// ```
33+
public struct FormatRawStringLiteral: RefactoringProvider {
34+
public static func refactor(syntax lit: StringLiteralExprSyntax, in context: Void) -> StringLiteralExprSyntax? {
35+
var maximumHashes = 0
36+
for segment in lit.segments {
37+
switch segment {
38+
case .expressionSegment(let expr):
39+
if let delimiter = expr.delimiter {
40+
// Pick up any delimiters in interpolation segments \#...#(...)
41+
maximumHashes = max(maximumHashes, delimiter.text.longestRun(of: "#"))
42+
}
43+
case .stringSegment(let string):
44+
// Find the longest run of # characters in the content of the literal.
45+
maximumHashes = max(maximumHashes, string.content.text.longestRun(of: "#"))
46+
}
47+
}
48+
49+
guard maximumHashes > 0 else {
50+
return lit
51+
.withOpenDelimiter(lit.openDelimiter?.withKind(.rawStringDelimiter("")))
52+
.withCloseDelimiter(lit.closeDelimiter?.withKind(.rawStringDelimiter("")))
53+
}
54+
55+
let delimiters = String(repeating: "#", count: maximumHashes + 1)
56+
return lit
57+
.withOpenDelimiter(lit.openDelimiter?.withKind(.rawStringDelimiter(delimiters)))
58+
.withCloseDelimiter(lit.closeDelimiter?.withKind(.rawStringDelimiter(delimiters)))
59+
}
60+
}
61+
62+
extension String {
63+
fileprivate func longestRun(of needle: Character) -> Int {
64+
var longest = 0
65+
var it = self.makeIterator()
66+
while let c = it.next() {
67+
guard c == needle else {
68+
continue
69+
}
70+
71+
var localLongest = 1
72+
while let c = it.next(), c == needle {
73+
localLongest += 1
74+
continue
75+
}
76+
77+
longest = max(localLongest, longest)
78+
}
79+
return longest
80+
}
81+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftRefactor
14+
import SwiftSyntaxBuilder
15+
@_spi(RawSyntax) import SwiftSyntax
16+
@_spi(RawSyntax) import SwiftParser
17+
18+
import XCTest
19+
import _SwiftSyntaxTestSupport
20+
21+
final class FormatRawStringLiteralTest: XCTestCase {
22+
func testDelimiterPlacement() throws {
23+
let tests = [
24+
(#line, literal: #" "Hello World" "#, expectation: #" "Hello World" "#),
25+
(#line, literal: ##" #"Hello World" "##, expectation: #" "Hello World" "#),
26+
(#line, literal: ##" #"Hello World"# "##, expectation: #" "Hello World" "#),
27+
(#line, literal: #####" "####" "#####, expectation: #####" "####" "#####),
28+
(#line, literal: #####" #"####"# "#####, expectation: ######" #####"####"##### "######),
29+
(#line, literal: #####" #"\####(hello)"# "#####, expectation: ######" #####"\####(hello)"##### "######),
30+
(#line, literal: #######" #"###### \####(hello) ##"# "#######, expectation: ########" #######"###### \####(hello) ##"####### "########),
31+
(#line, literal: ########" #######"hello \(world) "####### "########, expectation: #" "hello \(world) " "#),
32+
]
33+
34+
for (line, literal, expectation) in tests {
35+
let literal = try XCTUnwrap(StringLiteralExpr.parseWithoutDiagnostics(from: literal))
36+
let expectation = try XCTUnwrap(StringLiteralExpr.parseWithoutDiagnostics(from: expectation))
37+
let refactored = try XCTUnwrap(FormatRawStringLiteral.refactor(syntax: literal))
38+
AssertStringsEqualWithDiff(refactored.description, expectation.description, line: UInt(line))
39+
}
40+
}
41+
}
42+
43+
extension StringLiteralExpr {
44+
static func parseWithoutDiagnostics(from string: String) -> StringLiteralExpr? {
45+
var source = string
46+
source.makeContiguousUTF8()
47+
return source.withUTF8 { buffer in
48+
var parser = Parser(buffer)
49+
return parser.parseStringLiteral().syntax.as(StringLiteralExpr.self)
50+
}
51+
}
52+
}
53+

0 commit comments

Comments
 (0)