Skip to content

Commit 813a63a

Browse files
committed
Add \(literal: <expr>) interpolations
These convert certain common currency types into equivalent literals and insert them into the source code, automatically handling various edge cases like strings that require escaping or floating-point infinities.
1 parent 26669c5 commit 813a63a

File tree

3 files changed

+384
-5
lines changed

3 files changed

+384
-5
lines changed

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ extension SyntaxStringInterpolation: StringInterpolationProtocol {
109109
) {
110110
self.appendInterpolation(buildable.formatted(using: format))
111111
}
112+
113+
public mutating func appendInterpolation<Literal: ExpressibleByLiteralSyntax>(
114+
literal value: Literal,
115+
format: BasicFormat = BasicFormat()
116+
) {
117+
self.appendInterpolation(
118+
ExprSyntax(fromProtocol: value.makeLiteralSyntax()),
119+
format: format
120+
)
121+
}
112122
}
113123

114124
/// Syntax nodes that can be formed by a string interpolation involve source
@@ -135,6 +145,26 @@ enum SyntaxStringInterpolationError: Error, CustomStringConvertible {
135145
}
136146
}
137147

148+
/// A Swift type whose value can be represented directly in source code by a Swift literal.
149+
///
150+
/// Conforming types do not *contain* Swift source code; rather, they can be *expressed* in Swift source code, and this protocol can be used to get whatever source code would do that. For example, `String` is `ExpressibleByLiteralSyntax` but `StringLiteralExprSyntax` is not.
151+
///
152+
/// Conforming types can be interpolated into a Swift source code literal with the syntax `\(literal: <value>)`:
153+
///
154+
/// let greeting = "Hello, world!"
155+
/// let expr1 = ExprSyntax("print(\(literal: greeting))")
156+
/// // `expr1` is a syntax tree for `print("Hello, world!")`
157+
///
158+
/// Note that quote marks are automatically added around the contents; you don't have to write them yourself. The conformance will automatically ensure the contents are correctly escaped, possibly by using raw literals or other language features:
159+
///
160+
/// let msPath = "c:\\windows\\system32"
161+
/// let expr2 = ExprSyntax("open(\(literal: msPath))")
162+
/// // `expr2` might be a syntax tree for `open(#"c:\windows\system32"#)`
163+
/// // or for `open("c:\\windows\\system32")`.
164+
public protocol ExpressibleByLiteralSyntax {
165+
func makeLiteralSyntax() -> ExprSyntaxProtocol
166+
}
167+
138168
extension SyntaxExpressibleByStringInterpolation {
139169
/// Initialize a syntax node by parsing the contents of the interpolation.
140170
/// This function is marked `@_transparent` so that fatalErrors raised here
@@ -166,3 +196,218 @@ extension SyntaxExpressibleByStringInterpolation {
166196
try self.init(stringInterpolationOrThrow: interpolation)
167197
}
168198
}
199+
200+
// MARK: ExpressibleByLiteralSyntax conformances
201+
202+
extension Collection {
203+
fileprivate func allIndices(where predicate: @escaping (Element) -> Bool) -> UnfoldSequence<Index, Index> {
204+
sequence(state: startIndex) { i in
205+
guard let newI = self[i...].firstIndex(where: predicate) else {
206+
return nil
207+
}
208+
i = index(after: newI)
209+
return newI
210+
}
211+
}
212+
}
213+
214+
extension Substring: ExpressibleByLiteralSyntax {
215+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
216+
// TODO: Choose whether to use a single-line or multi-line literal.
217+
let quote = TokenSyntax.stringQuote
218+
219+
// Select a raw delimiter long enough that we won't need to escape quotes or backslashes.
220+
// Locate backslashes and quotes...
221+
let problemIndices = allIndices(where: #"\""#.contains(_:))
222+
// Count adjacent hashes and compute the largest number (-1 = no problem chars)...
223+
let maxPoundCount = problemIndices.reduce(-1) { prevCount, i in
224+
// Technically we don't need to check leading pounds for a backslash, but this is easier.
225+
Swift.max(
226+
prevCount,
227+
self[index(after: i)...].prefix(while: { $0 == "#" }).count,
228+
self[..<i].reversed().prefix(while: { $0 == "#" }).count
229+
)
230+
}
231+
// And create the delimiter.
232+
let rawDelimiter = String(repeating: "#", count: maxPoundCount + 1)
233+
234+
// Assemble the string with newlines escaped.
235+
var segment = ""
236+
var previousStart = startIndex
237+
238+
// Scan for next newline; if found, append text up to newline, then an escape sequence for the newline, then continue at the next character.
239+
for i in allIndices(where: \.isNewline) {
240+
segment += self[previousStart..<i]
241+
242+
for scalar in self[i].unicodeScalars {
243+
segment += "\\" + rawDelimiter
244+
switch scalar {
245+
case "\r":
246+
segment += "r"
247+
case "\n":
248+
segment += "n"
249+
default:
250+
segment += "u{\(String(scalar.value, radix: 16))}"
251+
}
252+
}
253+
254+
previousStart = index(after: i)
255+
}
256+
257+
// Append remainder of string.
258+
segment += self[previousStart...]
259+
260+
// Now make these into syntax nodes.
261+
let optRawDelimiter = rawDelimiter.isEmpty ? nil : rawDelimiter
262+
return StringLiteralExpr(
263+
openDelimiter: optRawDelimiter,
264+
openQuote: quote,
265+
segments: StringLiteralSegments {
266+
StringSegment(content: segment)
267+
},
268+
closeQuote: quote,
269+
closeDelimiter: optRawDelimiter
270+
)
271+
}
272+
}
273+
274+
extension String: ExpressibleByLiteralSyntax {
275+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
276+
self[...].makeLiteralSyntax()
277+
}
278+
}
279+
280+
extension ExpressibleByLiteralSyntax where Self: BinaryInteger {
281+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
282+
// TODO: Radix selection? Thousands separators?
283+
let digits = String(self, radix: 10)
284+
return IntegerLiteralExpr(digits: digits)
285+
}
286+
}
287+
extension Int: ExpressibleByLiteralSyntax {}
288+
extension Int8: ExpressibleByLiteralSyntax {}
289+
extension Int16: ExpressibleByLiteralSyntax {}
290+
extension Int32: ExpressibleByLiteralSyntax {}
291+
extension Int64: ExpressibleByLiteralSyntax {}
292+
extension UInt: ExpressibleByLiteralSyntax {}
293+
extension UInt8: ExpressibleByLiteralSyntax {}
294+
extension UInt16: ExpressibleByLiteralSyntax {}
295+
extension UInt32: ExpressibleByLiteralSyntax {}
296+
extension UInt64: ExpressibleByLiteralSyntax {}
297+
298+
extension ExpressibleByLiteralSyntax where Self: FloatingPoint, Self: LosslessStringConvertible {
299+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
300+
switch floatingPointClass {
301+
case .positiveInfinity:
302+
return MemberAccessExpr(name: "infinity")
303+
304+
case .quietNaN:
305+
return MemberAccessExpr(name: "nan")
306+
307+
case .signalingNaN:
308+
return MemberAccessExpr(name: "signalingNaN")
309+
310+
case .negativeInfinity, .negativeZero:
311+
return PrefixOperatorExpr(
312+
operatorToken: "-",
313+
postfixExpression: (-self).makeLiteralSyntax()
314+
)
315+
316+
case .negativeNormal, .negativeSubnormal, .positiveZero, .positiveSubnormal, .positiveNormal:
317+
// TODO: Thousands separators?
318+
let digits = String(self)
319+
return FloatLiteralExpr(floatingDigits: digits)
320+
}
321+
322+
}
323+
}
324+
extension Float: ExpressibleByLiteralSyntax {}
325+
extension Double: ExpressibleByLiteralSyntax {}
326+
327+
#if !((os(macOS) || targetEnvironment(macCatalyst)) && arch(x86_64))
328+
@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
329+
extension Float16: ExpressibleByLiteralSyntax {}
330+
#endif
331+
332+
extension Bool: ExpressibleByLiteralSyntax {
333+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
334+
BooleanLiteralExpr(self)
335+
}
336+
}
337+
338+
extension ArraySlice: ExpressibleByLiteralSyntax where Element: ExpressibleByLiteralSyntax {
339+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
340+
ArrayExpr(
341+
leftSquare: .leftSquareBracket,
342+
elements: ArrayElementList {
343+
for elem in self {
344+
ArrayElement(expression: elem.makeLiteralSyntax())
345+
}
346+
},
347+
rightSquare: .rightSquareBracket
348+
)
349+
}
350+
}
351+
352+
extension Array: ExpressibleByLiteralSyntax where Element: ExpressibleByLiteralSyntax {
353+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
354+
self[...].makeLiteralSyntax()
355+
}
356+
}
357+
358+
extension Set: ExpressibleByLiteralSyntax where Element: ExpressibleByLiteralSyntax {
359+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
360+
// Sets are unordered. Sort the elements by their source-code representation to emit them in a stable order.
361+
let elemSyntaxes = map {
362+
$0.makeLiteralSyntax()
363+
}.sorted {
364+
$0.syntaxTextBytes.lexicographicallyPrecedes($1.syntaxTextBytes)
365+
}
366+
367+
return ArrayExpr(
368+
leftSquare: .leftSquareBracket,
369+
elements: ArrayElementList {
370+
for elemSyntax in elemSyntaxes {
371+
ArrayElement(expression: elemSyntax)
372+
}
373+
},
374+
rightSquare: .rightSquareBracket
375+
)
376+
}
377+
}
378+
379+
extension KeyValuePairs: ExpressibleByLiteralSyntax where Key: ExpressibleByLiteralSyntax, Value: ExpressibleByLiteralSyntax {
380+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
381+
DictionaryExpr(leftSquare: .leftSquareBracket, rightSquare: .rightSquareBracket) {
382+
for elem in self {
383+
DictionaryElement(
384+
keyExpression: elem.key.makeLiteralSyntax(),
385+
colon: .colon,
386+
valueExpression: elem.value.makeLiteralSyntax()
387+
)
388+
}
389+
}
390+
}
391+
}
392+
393+
extension Dictionary: ExpressibleByLiteralSyntax where Key: ExpressibleByLiteralSyntax, Value: ExpressibleByLiteralSyntax {
394+
public func makeLiteralSyntax() -> ExprSyntaxProtocol {
395+
// Dictionaries are unordered. Sort the elements by their keys' source-code representation to emit them in a stable order.
396+
let elemSyntaxes = map {
397+
(key: $0.key.makeLiteralSyntax(), value: $0.value.makeLiteralSyntax())
398+
}.sorted {
399+
$0.key.syntaxTextBytes.lexicographicallyPrecedes($1.key.syntaxTextBytes)
400+
}
401+
402+
return DictionaryExpr(leftSquare: .leftSquareBracket, rightSquare: .rightSquareBracket) {
403+
for elemSyntax in elemSyntaxes {
404+
DictionaryElement(
405+
keyExpression: elemSyntax.key,
406+
colon: .colon,
407+
valueExpression: elemSyntax.value
408+
)
409+
}
410+
}
411+
}
412+
}
413+

Sources/_SwiftSyntaxMacros/MacroSystem+Builtin.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public struct ColumnMacro: ExpressionMacro {
2020
let line = macro.startLocation(
2121
converter: context.sourceLocationConverter
2222
).column ?? 0
23-
return .init("\(line)")
23+
return .init("\(literal: line)")
2424
}
2525
}
2626

@@ -31,7 +31,7 @@ public struct LineMacro: ExpressionMacro {
3131
let line = macro.startLocation(
3232
converter: context.sourceLocationConverter
3333
).line ?? 0
34-
return .init("\(line)")
34+
return .init("\(literal: line)")
3535
}
3636
}
3737

@@ -141,7 +141,7 @@ public struct FunctionMacro: ExpressionMacro {
141141
_ macro: MacroExpansionExprSyntax, in context: MacroEvaluationContext
142142
) -> MacroResult<ExprSyntax> {
143143
let name = findEnclosingName(macro) ?? context.moduleName
144-
let literal: ExprSyntax = #""\#(name)""#
144+
let literal: ExprSyntax = "\(literal: name)"
145145
if let leadingTrivia = macro.leadingTrivia {
146146
return .init(literal.withLeadingTrivia(leadingTrivia))
147147
}
@@ -215,7 +215,7 @@ public struct FilePathMacro: ExpressionMacro {
215215
let fileName = context.sourceLocationConverter.location(
216216
for: .init(utf8Offset: 0)
217217
).file ?? "<unknown file>"
218-
let fileLiteral: ExprSyntax = #""\#(fileName)""#
218+
let fileLiteral: ExprSyntax = "\(literal: fileName)"
219219
if let leadingTrivia = macro.leadingTrivia {
220220
return MacroResult(fileLiteral.withLeadingTrivia(leadingTrivia))
221221
}
@@ -236,7 +236,10 @@ public struct FileIDMacro: ExpressionMacro {
236236
fileName = String(fileName[fileName.index(after: lastSlash)...])
237237
}
238238

239-
let fileLiteral: ExprSyntax = #""\#(context.moduleName)/\#(fileName)""#
239+
// FIXME: Compiler has more sophisticated file ID computation
240+
let fileID = "\(context.moduleName)/\(fileName)"
241+
242+
let fileLiteral: ExprSyntax = "\(literal: fileID)"
240243
if let leadingTrivia = macro.leadingTrivia {
241244
return MacroResult(fileLiteral.withLeadingTrivia(leadingTrivia))
242245
}

0 commit comments

Comments
 (0)