Skip to content

Commit 2b3a28f

Browse files
committed
Support Optional literals in builder interpolation
1 parent 3cd9dfa commit 2b3a28f

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ extension SyntaxStringInterpolation: StringInterpolationProtocol {
119119
format: format
120120
)
121121
}
122+
123+
// This overload is technically redundant with the previous one, except that
124+
// it silences a warning about interpolating Optionals.
125+
public mutating func appendInterpolation<Literal: ExpressibleByLiteralSyntax>(
126+
literal value: Literal?,
127+
format: BasicFormat = BasicFormat()
128+
) {
129+
self.appendInterpolation(
130+
ExprSyntax(fromProtocol: value.makeLiteralSyntax()),
131+
format: format
132+
)
133+
}
122134
}
123135

124136
/// Syntax nodes that can be formed by a string interpolation involve source
@@ -346,3 +358,47 @@ extension Dictionary: ExpressibleByLiteralSyntax where Key: ExpressibleByLiteral
346358
}
347359
}
348360

361+
extension Optional: ExpressibleByLiteralSyntax where Wrapped: ExpressibleByLiteralSyntax {
362+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
363+
func containsNil(_ expr: ExprSyntaxProtocol) -> Bool {
364+
if expr.is(NilLiteralExpr.self) {
365+
return true
366+
}
367+
368+
if let call = expr.as(FunctionCallExpr.self),
369+
let memberAccess = call.calledExpression.as(MemberAccessExpr.self),
370+
memberAccess.name.text == "some",
371+
let argument = call.argumentList.first?.expression {
372+
return containsNil(argument)
373+
}
374+
375+
return false
376+
}
377+
378+
switch self {
379+
case nil:
380+
return NilLiteralExpr()
381+
382+
case let wrapped?:
383+
let wrappedExpr = wrapped.makeLiteralSyntax()
384+
385+
// If this is a nested optional type, and the wrapped value is `nil` or
386+
// e.g. `.some(nil)`, add a layer of `.some(_:)` around it to preserve the
387+
// depth of the data structure.
388+
if containsNil(wrappedExpr) {
389+
// TODO: Can't we have something nicer than this? `MemberAccessExpr(name: "some").makeCall(wrapped)`?
390+
return FunctionCallExpr(
391+
calledExpression: MemberAccessExpr(name: "some"),
392+
leftParen: .leftParen,
393+
argumentList: TupleExprElementList {
394+
TupleExprElement(expression: wrappedExpr)
395+
},
396+
rightParen: .rightParen
397+
)
398+
}
399+
400+
return wrappedExpr
401+
}
402+
}
403+
}
404+

Tests/SwiftSyntaxBuilderTest/StringInterpolation.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,26 @@ final class StringInterpolationTests: XCTestCase {
203203
AssertStringsEqualWithDiff(d.description, #"print([1: "one", 2: "two", 3: "three"])"#)
204204
}
205205

206+
func testInterpolationLiteralOptional() {
207+
let some: Optional<Int> = 42
208+
let none: Optional<Int> = nil
209+
210+
let a: ExprSyntax = "print(\(literal: some))"
211+
AssertStringsEqualWithDiff(a.description, #"print(42)"#)
212+
213+
let b: ExprSyntax = "print(\(literal: Optional.some(some)))"
214+
AssertStringsEqualWithDiff(b.description, #"print(42)"#)
215+
216+
let c: ExprSyntax = "print(\(literal: none))"
217+
AssertStringsEqualWithDiff(c.description, #"print(nil)"#)
218+
219+
let d: ExprSyntax = "print(\(literal: Optional.some(none)))"
220+
AssertStringsEqualWithDiff(d.description, #"print(.some(nil))"#)
221+
222+
let e: ExprSyntax = "print(\(literal: Int??.none))"
223+
AssertStringsEqualWithDiff(e.description, #"print(nil)"#)
224+
}
225+
206226
func testRewriter() {
207227
let sourceFile = Parser.parse(source: """
208228
class Foo {

0 commit comments

Comments
 (0)