Skip to content

Commit 1d64ead

Browse files
committed
Convert String Concatenation to String Interpolation
added `ConvertStringConcatenationToStringInterpolation` to convert string concatenation to string interpolation: - the string concatenation must contain at least one string literal - the number of pound symbols in the resulting string interpolation is determined by the highest number of pound symbols among all string literals in the string concatenation - multiline string literals are not yet supported registered in `SyntaxCodeActions.allSyntaxCodeActions` registered in Sources/SourceKitLSP/CMakeLists added tests in `CodeActionTests`
1 parent 607292a commit 1d64ead

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ target_sources(SourceKitLSP PRIVATE
3131
Swift/CodeActions/AddDocumentation.swift
3232
Swift/CodeActions/ConvertIntegerLiteral.swift
3333
Swift/CodeActions/ConvertJSONToCodableStruct.swift
34+
Swift/CodeActions/ConvertStringConcatenationToStringInterpolation.swift,
3435
Swift/CodeActions/PackageManifestEdits.swift
3536
Swift/CodeActions/SyntaxCodeActionProvider.swift
3637
Swift/CodeActions/SyntaxCodeActions.swift
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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 LanguageServerProtocol
14+
import SwiftRefactor
15+
import SwiftSyntax
16+
17+
struct ConvertStringConcatenationToStringInterpolation: SyntaxRefactoringProvider {
18+
static func refactor(syntax: SequenceExprSyntax, in context: Void) -> SequenceExprSyntax? {
19+
guard let (componentsOnly, commonPounds) = preflight(exprList: syntax.elements) else {
20+
return nil
21+
}
22+
23+
var ret: StringLiteralSegmentListSyntax = []
24+
for component in componentsOnly {
25+
if let stringLiteral = StringLiteralExprSyntax(component) {
26+
var segments = stringLiteral.segments
27+
if stringLiteral.openingPounds?.tokenKind != commonPounds?.tokenKind {
28+
for i in segments.indices {
29+
guard case var .expressionSegment(exprSegment) = segments[i] else {
30+
continue
31+
}
32+
exprSegment.pounds = commonPounds
33+
segments[i] = .expressionSegment(exprSegment)
34+
}
35+
}
36+
ret += segments
37+
} else {
38+
ret.append(
39+
.expressionSegment(
40+
ExpressionSegmentSyntax(
41+
pounds: commonPounds,
42+
expressions: [
43+
LabeledExprSyntax(expression: component.trimmed)
44+
]
45+
)
46+
)
47+
)
48+
}
49+
}
50+
51+
return SequenceExprSyntax(elements: [
52+
ExprSyntax(
53+
StringLiteralExprSyntax(
54+
openingPounds: commonPounds,
55+
openingQuote: "\"",
56+
segments: ret,
57+
closingQuote: "\"",
58+
closingPounds: commonPounds
59+
)
60+
)
61+
])
62+
}
63+
64+
/// If `exprList` is a string concatenation, returns 1) all elements in `exprList` with concat operators stripped and 2) the longest pounds amongst all string literals,
65+
/// otherwise returns nil.
66+
///
67+
/// `exprList` as a valid string concatenation must contain n >= 3 children where n is an odd number with a concat operator `+` separating every other child which must either be a string literal or a valid expression for string interpolation. `exprList` must also contain at least one string literal child.
68+
///
69+
/// For example,
70+
///
71+
/// "Hello " + aString + "\(1)World"
72+
/// is a valid string concatenation.
73+
///
74+
/// aString + bString
75+
/// and
76+
///
77+
/// "Hello " * aString - "World"
78+
/// are invalid string concatenations.
79+
static func preflight(
80+
exprList: ExprListSyntax
81+
) -> (componentsOnly: [ExprListSyntax.Element], longestPounds: TokenSyntax?)? {
82+
var iter = exprList.makeIterator()
83+
guard let first = iter.next() else {
84+
return nil
85+
}
86+
87+
var hasStringLiterals = false
88+
var longestPounds: TokenSyntax?
89+
var componentsOnly = [first]
90+
91+
if let stringLiteral = StringLiteralExprSyntax(first) {
92+
hasStringLiterals = true
93+
longestPounds = stringLiteral.openingPounds
94+
}
95+
96+
while let concat = iter.next(), let stringComponent = iter.next() {
97+
guard let concat = BinaryOperatorExprSyntax(concat), case .binaryOperator("+") = concat.operator.tokenKind else {
98+
return nil
99+
}
100+
101+
if let stringLiteral = StringLiteralExprSyntax(stringComponent) {
102+
hasStringLiterals = true
103+
if let pounds = stringLiteral.openingPounds,
104+
pounds.trimmedLength > (longestPounds?.trimmedLength ?? SourceLength(utf8Length: 0))
105+
{
106+
longestPounds = pounds
107+
}
108+
}
109+
110+
componentsOnly.append(stringComponent)
111+
}
112+
113+
guard hasStringLiterals && componentsOnly.count > 1 else {
114+
return nil
115+
}
116+
117+
return (componentsOnly, longestPounds)
118+
}
119+
120+
}
121+
122+
extension ConvertStringConcatenationToStringInterpolation: SyntaxRefactoringCodeActionProvider {
123+
static let title: String = "Convert String Concatenation to String Interpolation"
124+
125+
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> SequenceExprSyntax? {
126+
guard let token = scope.innermostNodeContainingRange,
127+
let seqExpr = token.findParentOfSelf(
128+
ofType: SequenceExprSyntax.self,
129+
stoppingIf: {
130+
$0.kind == .codeBlockItem || $0.kind == .memberBlockItem
131+
}
132+
)
133+
else {
134+
return nil
135+
}
136+
137+
return seqExpr
138+
}
139+
}

Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ let allSyntaxCodeActions: [SyntaxCodeActionProvider.Type] = [
1919
AddSeparatorsToIntegerLiteral.self,
2020
ConvertIntegerLiteral.self,
2121
ConvertJSONToCodableStruct.self,
22+
ConvertStringConcatenationToStringInterpolation.self,
2223
FormatRawStringLiteral.self,
2324
MigrateToNewIfLetSyntax.self,
2425
OpaqueParameterToGeneric.self,

Tests/SourceKitLSPTests/CodeActionTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,64 @@ final class CodeActionTests: XCTestCase {
10061006
}
10071007
}
10081008

1009+
func testConvertStringConcatenationToStringInterpolation() async throws {
1010+
try await assertCodeActions(
1011+
"""
1012+
1️⃣#"["# + 2️⃣key + ": \\(3️⃣d) " + 4️⃣value + ##"]"##5️⃣
1013+
""",
1014+
ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")],
1015+
exhaustive: false
1016+
) { uri, positions in
1017+
[
1018+
CodeAction(
1019+
title: "Convert String Concatenation to String Interpolation",
1020+
kind: .refactorInline,
1021+
edit: WorkspaceEdit(
1022+
changes: [
1023+
uri: [
1024+
TextEdit(
1025+
range: positions["1️⃣"]..<positions["5️⃣"],
1026+
newText: """
1027+
##"[\\##(key): \\##(d) \\##(value)]"##
1028+
"""
1029+
)
1030+
]
1031+
]
1032+
)
1033+
)
1034+
]
1035+
}
1036+
}
1037+
1038+
func testConvertStringConcatenationToStringInterpolationNotShowUp() async throws {
1039+
// ""
1040+
try await assertCodeActions(
1041+
"""
1042+
1️⃣##"[\\##(2️⃣key): \\##(3️⃣d) 4️⃣\\##(value)]"##5️⃣
1043+
""",
1044+
ranges: [("1️⃣", "2️⃣"), ("3️⃣", "4️⃣"), ("1️⃣", "5️⃣")]
1045+
) { uri, positions in
1046+
[
1047+
CodeAction(
1048+
title: "Convert string literal to minimal number of \'#\'s",
1049+
kind: .refactorInline,
1050+
edit: WorkspaceEdit(
1051+
changes: [
1052+
uri: [
1053+
TextEdit(
1054+
range: positions["1️⃣"]..<positions["5️⃣"],
1055+
newText: """
1056+
###"[\\##(key): \\##(d) \\##(value)]"###
1057+
"""
1058+
)
1059+
]
1060+
]
1061+
)
1062+
)
1063+
]
1064+
}
1065+
}
1066+
10091067
/// Retrieves the code action at a set of markers and asserts that it matches a list of expected code actions.
10101068
///
10111069
/// - Parameters:

0 commit comments

Comments
 (0)