Skip to content

Commit 0780aca

Browse files
authored
Merge pull request #22433 from github/jketema/swift-json
Unified: Hand-roll the mashalling of the JSON
2 parents 3ee0030 + f6efc34 commit 0780aca

3 files changed

Lines changed: 116 additions & 6 deletions

File tree

unified/swift-syntax-rs/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ cc_library(
3636
cc_library(
3737
name = "static_runtime_group_end",
3838
linkopts = [
39+
"-lBlocksRuntime",
3940
"-lc",
41+
"-ldispatch",
4042
"-ldl",
4143
"-lm",
4244
"-lpthread",

unified/swift-syntax-rs/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,27 @@ mod tests {
120120
json.contains("\"kind\":\"sourceFile\""),
121121
"unexpected tree: {json}"
122122
);
123+
assert!(
124+
json.contains("\"statements\":[]"),
125+
"empty collections should be serialized as JSON arrays: {json}"
126+
);
127+
}
128+
129+
#[test]
130+
fn serializes_json_strings_and_keys_deterministically() {
131+
let source = "/* quote \" slash / backslash \\ tab \t newline\n emoji 😀 combining e\u{301} control \u{1} */\nlet x = 1";
132+
let json = parse_to_json(source).expect("parsing should succeed");
133+
134+
assert!(
135+
json.contains(
136+
r#""text":"\/* quote \" slash \/ backslash \\ tab \t newline\n emoji 😀 combining é control \u0001 *\/""#
137+
),
138+
"JSON string was not escaped correctly: {json}"
139+
);
140+
assert!(
141+
json.contains(r#""start":{"column":1,"line":1,"offset":0}"#),
142+
"JSON object keys were not sorted: {json}"
143+
);
123144
}
124145

125146
#[test]

unified/swift-syntax-rs/swift/Sources/SwiftSyntaxFFI/SwiftSyntaxFFI.swift

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Foundation
21
import SwiftOperators
32
import SwiftParser
43

@@ -214,6 +213,93 @@ private final class PerSequenceFolder: SyntaxRewriter {
214213
}
215214
}
216215

216+
/// Write directly to stderr without Foundation, whose output APIs would pull ICU into static builds.
217+
private func writeToStandardError(_ message: String) {
218+
message.withCString { start in
219+
var pointer = UnsafeRawPointer(start)
220+
let end = pointer.advanced(by: strlen(start))
221+
while pointer != end {
222+
let written = write(STDERR_FILENO, pointer, end - pointer)
223+
if written <= 0 {
224+
return
225+
}
226+
pointer = pointer.advanced(by: written)
227+
}
228+
}
229+
}
230+
231+
private struct JSONEncodingError: Error, CustomStringConvertible {
232+
let type: Any.Type
233+
234+
var description: String {
235+
"unsupported JSON value of type \(String(reflecting: type))"
236+
}
237+
}
238+
239+
private let hexDigits = Array("0123456789abcdef".utf8)
240+
241+
private func appendJSONString(_ string: String, to output: inout [UInt8]) {
242+
output.append(UInt8(ascii: "\""))
243+
for scalar in string.unicodeScalars {
244+
switch scalar.value {
245+
case 0x08:
246+
output.append(contentsOf: "\\b".utf8)
247+
case 0x09:
248+
output.append(contentsOf: "\\t".utf8)
249+
case 0x0a:
250+
output.append(contentsOf: "\\n".utf8)
251+
case 0x0c:
252+
output.append(contentsOf: "\\f".utf8)
253+
case 0x0d:
254+
output.append(contentsOf: "\\r".utf8)
255+
case 0x22:
256+
output.append(contentsOf: "\\\"".utf8)
257+
case 0x2f:
258+
output.append(contentsOf: "\\/".utf8)
259+
case 0x5c:
260+
output.append(contentsOf: "\\\\".utf8)
261+
case 0x00...0x1f:
262+
output.append(contentsOf: "\\u00".utf8)
263+
output.append(hexDigits[Int(scalar.value >> 4)])
264+
output.append(hexDigits[Int(scalar.value & 0x0f)])
265+
default:
266+
output.append(contentsOf: String(scalar).utf8)
267+
}
268+
}
269+
output.append(UInt8(ascii: "\""))
270+
}
271+
272+
private func appendJSON(_ value: Any, to output: inout [UInt8]) throws {
273+
switch value {
274+
case let string as String:
275+
appendJSONString(string, to: &output)
276+
case let integer as Int:
277+
output.append(contentsOf: String(integer).utf8)
278+
case let array as [Any]:
279+
output.append(UInt8(ascii: "["))
280+
for (index, element) in array.enumerated() {
281+
if index != 0 {
282+
output.append(UInt8(ascii: ","))
283+
}
284+
try appendJSON(element, to: &output)
285+
}
286+
output.append(UInt8(ascii: "]"))
287+
case let object as [String: Any]:
288+
output.append(UInt8(ascii: "{"))
289+
for (index, key) in object.keys.sorted().enumerated() {
290+
if index != 0 {
291+
output.append(UInt8(ascii: ","))
292+
}
293+
appendJSONString(key, to: &output)
294+
output.append(UInt8(ascii: ":"))
295+
try appendJSON(object[key]!, to: &output)
296+
}
297+
output.append(UInt8(ascii: "}"))
298+
default:
299+
throw JSONEncodingError(type: type(of: value))
300+
}
301+
}
302+
217303
/// Parse the given NUL-terminated Swift source string and return a
218304
/// heap-allocated, NUL-terminated JSON representation of the syntax tree.
219305
///
@@ -231,13 +317,14 @@ public func ssr_parse_json(_ source: UnsafePointer<CChar>?) -> UnsafeMutablePoin
231317
let converter = SourceLocationConverter(fileName: "<input>", tree: tree)
232318
let json = serialize(folded, converter)
233319

234-
guard
235-
let data = try? JSONSerialization.data(
236-
withJSONObject: json, options: [.sortedKeys]),
237-
let string = String(data: data, encoding: .utf8)
238-
else {
320+
var bytes: [UInt8] = []
321+
do {
322+
try appendJSON(json, to: &bytes)
323+
} catch {
324+
writeToStandardError("SwiftSyntaxFFI: JSON serialization failed: \(error)\n")
239325
return nil
240326
}
327+
let string = String(decoding: bytes, as: UTF8.self)
241328
return strdup(string)
242329
}
243330

0 commit comments

Comments
 (0)