@@ -90,6 +90,42 @@ extension DictionaryExpr {
90
90
}
91
91
}
92
92
93
+ // MARK: - Expr
94
+
95
+ extension Expr {
96
+ /// Returns a syntax tree for an expression that represents the value of the
97
+ /// provided instance. For example, passing an `Array<String>` will result in
98
+ /// an array literal containing string literals:
99
+ ///
100
+ /// let arrayExpr = Expr(literal: ["a", "b", "c"])
101
+ /// // `arrayExpr` is a syntax tree like `["a", "b", "c"]`
102
+ ///
103
+ /// This initializer is compatible with types that conform to
104
+ /// ``ExpressibleByLiteralSyntax``. These include:
105
+ ///
106
+ /// * `String` and `Substring`
107
+ /// * `Int` and other integer types
108
+ /// * `Double` and other floating-point types
109
+ /// * `Bool`
110
+ /// * `Array` and `Set` of conforming elements
111
+ /// * `Dictionary` and `KeyValuePairs` of conforming keys and values
112
+ /// * `Optional` of conforming wrapped value
113
+ ///
114
+ /// Conformances will generally handle edge cases sensibly: `String` will
115
+ /// use raw literals and escapes as needed, `Optional` will wrap a nested
116
+ /// `nil` in `.some`, `Double` wil represent special values like infinities
117
+ /// as code sequences like `.infinity`, etc. `Set` and `Dictionary` sort
118
+ /// thier elements to improve stability.
119
+ ///
120
+ /// Because of that intelligent behavior, this initializer is not guaranteed
121
+ /// to produce a literal as the outermost syntax node, or even to have a
122
+ /// literal anywhere in its syntax tree. Use a convenience initializer on a
123
+ /// specific type if you need that exact type in the syntax tree.
124
+ public init < Literal: ExpressibleByLiteralSyntax > ( literal: Literal ) {
125
+ self . init ( fromProtocol: literal. makeLiteralSyntax ( ) )
126
+ }
127
+ }
128
+
93
129
// MARK: - FloatLiteralExprSyntax
94
130
95
131
extension FloatLiteralExprSyntax : ExpressibleByFloatLiteral {
@@ -105,6 +141,7 @@ extension FloatLiteralExprSyntax: ExpressibleByFloatLiteral {
105
141
// MARK: - FunctionCallExpr
106
142
107
143
extension FunctionCallExpr {
144
+ // Need an overload that's explicitly `ExprSyntax` for code literals to work.
108
145
/// A convenience initializer that allows passing in arguments using a result builder
109
146
/// instead of having to wrap them in a `TupleExprElementList`.
110
147
/// The presence of the parenthesis will be inferred based on the presence of arguments and the trailing closure.
@@ -125,6 +162,23 @@ extension FunctionCallExpr {
125
162
additionalTrailingClosures: additionalTrailingClosures
126
163
)
127
164
}
165
+
166
+ /// A convenience initializer that allows passing in arguments using a result builder
167
+ /// instead of having to wrap them in a `TupleExprElementList`.
168
+ /// The presence of the parenthesis will be inferred based on the presence of arguments and the trailing closure.
169
+ public init (
170
+ callee: ExprSyntaxProtocol ,
171
+ trailingClosure: ClosureExprSyntax ? = nil ,
172
+ additionalTrailingClosures: MultipleTrailingClosureElementList ? = nil ,
173
+ @TupleExprElementListBuilder argumentList: ( ) -> TupleExprElementList = { [ ] }
174
+ ) {
175
+ self . init (
176
+ callee: ExprSyntax ( fromProtocol: callee) ,
177
+ trailingClosure: trailingClosure,
178
+ additionalTrailingClosures: additionalTrailingClosures,
179
+ argumentList: argumentList
180
+ )
181
+ }
128
182
}
129
183
130
184
// MARK: - FunctionParameter
@@ -199,24 +253,39 @@ extension MemberAccessExpr {
199
253
// MARK: - StringLiteralExpr
200
254
201
255
extension String {
202
- /// Replace literal newlines with "\r", "\n".
203
- fileprivate func replacingNewlines( ) -> String {
204
- var result = " "
205
- var input = self [ ... ]
206
- while let firstNewline = input. firstIndex ( where: { $0. isNewline } ) {
256
+ /// Replace literal newlines with "\r", "\n", "\u{2028}", and ASCII control characters with "\0", "\u{7}"
257
+ fileprivate func escapingForStringLiteral( usingDelimiter delimiter: String ) -> String {
258
+ // String literals cannot contain "unprintable" ASCII characters (control
259
+ // characters, etc.) besides tab. As a matter of style, we also choose to
260
+ // escape Unicode newlines like "\u{2028}" even though swiftc will allow
261
+ // them in string literals.
262
+ func needsEscaping( _ scalar: UnicodeScalar ) -> Bool {
263
+ return ( scalar. isASCII && scalar != " \t " && !scalar. isPrintableASCII)
264
+ || Character ( scalar) . isNewline
265
+ }
266
+
267
+ // Work at the Unicode scalar level so that "\r\n" isn't combined.
268
+ var result = String . UnicodeScalarView ( )
269
+ var input = self . unicodeScalars [ ... ]
270
+ while let firstNewline = input. firstIndex ( where: needsEscaping ( _: ) ) {
207
271
result += input [ ..< firstNewline]
208
- if input [ firstNewline] == " \r " {
209
- result += " \\ r "
210
- } else if input [ firstNewline] == " \r \n " {
211
- result += " \\ r \\ n "
212
- } else {
213
- result += " \\ n "
272
+
273
+ result += " \\ \( delimiter) " . unicodeScalars
274
+ switch input [ firstNewline] {
275
+ case " \r " :
276
+ result += " r " . unicodeScalars
277
+ case " \n " :
278
+ result += " n " . unicodeScalars
279
+ case " \0 " :
280
+ result += " 0 " . unicodeScalars
281
+ case let other:
282
+ result += " u{ \( String ( other. value, radix: 16 ) ) } " . unicodeScalars
214
283
}
215
284
input = input [ input. index ( after: firstNewline) ... ]
216
- continue
217
285
}
286
+ result += input
218
287
219
- return result + input
288
+ return String ( result)
220
289
}
221
290
}
222
291
@@ -226,34 +295,28 @@ extension StringLiteralExpr {
226
295
}
227
296
228
297
private static func requiresEscaping( _ content: String ) -> ( Bool , poundCount: Int ) {
229
- var state : PoundState = . none
298
+ var countingPounds = false
230
299
var consecutivePounds = 0
231
300
var maxPounds = 0
232
301
var requiresEscaping = false
233
302
234
303
for c in content {
235
- switch c {
236
- case " # " :
304
+ switch ( countingPounds, c) {
305
+ // Normal mode: scanning for characters that can be followed by pounds.
306
+ case ( false , " \" " ) , ( false , " \\ " ) :
307
+ countingPounds = true
308
+ requiresEscaping = true
309
+ case ( false , _) :
310
+ continue
311
+
312
+ // Special mode: counting a sequence of pounds until we reach its end.
313
+ case ( true , " # " ) :
237
314
consecutivePounds += 1
238
- case " \" " :
239
- state = . afterQuote
240
- consecutivePounds = 0
241
- case " \\ " :
242
- state = . afterBackslash
243
- consecutivePounds = 0
244
- case " ( " where state == . afterBackslash:
245
315
maxPounds = max ( maxPounds, consecutivePounds)
246
- fallthrough
247
- default :
316
+ case ( true , _ ) :
317
+ countingPounds = false
248
318
consecutivePounds = 0
249
- state = . none
250
- }
251
-
252
- if state == . afterQuote {
253
- maxPounds = max ( maxPounds, consecutivePounds)
254
319
}
255
-
256
- requiresEscaping = requiresEscaping || state != . none
257
320
}
258
321
259
322
return ( requiresEscaping, poundCount: maxPounds)
@@ -269,10 +332,6 @@ extension StringLiteralExpr {
269
332
closeQuote: Token = . stringQuote,
270
333
closeDelimiter: Token ? = nil
271
334
) {
272
- let contentToken = Token . stringSegment ( content. replacingNewlines ( ) )
273
- let segment = StringSegment ( content: contentToken)
274
- let segments = StringLiteralSegments ( [ . stringSegment( segment) ] )
275
-
276
335
var openDelimiter = openDelimiter
277
336
var closeDelimiter = closeDelimiter
278
337
if openDelimiter == nil , closeDelimiter == nil {
@@ -285,6 +344,11 @@ extension StringLiteralExpr {
285
344
}
286
345
}
287
346
347
+ let escapedContent = content. escapingForStringLiteral ( usingDelimiter: closeDelimiter? . text ?? " " )
348
+ let contentToken = Token . stringSegment ( escapedContent)
349
+ let segment = StringSegment ( content: contentToken)
350
+ let segments = StringLiteralSegments ( [ . stringSegment( segment) ] )
351
+
288
352
self . init (
289
353
openDelimiter: openDelimiter,
290
354
openQuote: openQuote,
0 commit comments