Skip to content

Commit 4db7b1f

Browse files
authored
Merge pull request swiftlang#1911 from hartbit/swift-compiler-output-parser
Support parsing the swift compiler json output
2 parents 82d2483 + 91e39d8 commit 4db7b1f

File tree

3 files changed

+522
-0
lines changed

3 files changed

+522
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2019 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
import Basic
13+
14+
/// Represents a message output by the Swift compiler in JSON output mode.
15+
struct SwiftCompilerMessage {
16+
enum Kind {
17+
struct Output {
18+
let type: String
19+
let path: String
20+
}
21+
22+
struct CommandInfo {
23+
let inputs: [String]
24+
let outputs: [Output]
25+
}
26+
27+
struct OutputInfo {
28+
let output: String?
29+
}
30+
31+
case began(CommandInfo)
32+
case skipped(CommandInfo)
33+
case finished(OutputInfo)
34+
case signalled(OutputInfo)
35+
}
36+
37+
let name: String
38+
let kind: Kind
39+
}
40+
41+
/// Protocol for the parser delegate to get notified of parsing events.
42+
protocol SwiftCompilerOutputParserDelegate: class {
43+
/// Called for each message parsed.
44+
func swiftCompilerDidOutputMessage(_ message: SwiftCompilerMessage)
45+
46+
/// Called on an un-expected parsing error. No more events will be received after that.
47+
func swiftCompilerOutputParserDidFail(withError error: Error)
48+
}
49+
50+
/// Parser for the Swift compiler JSON output mode.
51+
final class SwiftCompilerOutputParser {
52+
53+
/// State of the parser state machine.
54+
private enum State {
55+
case parsingMessageSize
56+
case parsingMessage(size: Int)
57+
case parsingNewlineAfterMessage
58+
}
59+
60+
/// Delegate to notify of parsing events.
61+
public var delegate: SwiftCompilerOutputParserDelegate
62+
63+
/// Buffer containing the bytes until a full message can be parsed.
64+
private var buffer: [UInt8] = []
65+
66+
/// The parser's state machine current state.
67+
private var state: State = .parsingMessageSize
68+
69+
/// Boolean indicating if the parser has encountered an un-expected parsing error.
70+
private var hasFailed = false
71+
72+
/// The JSON decoder to parse messages.
73+
private let decoder = JSONDecoder()
74+
75+
/// Initializes the parser with a delegate to notify of parsing events.
76+
init(delegate: SwiftCompilerOutputParserDelegate) {
77+
self.delegate = delegate
78+
}
79+
80+
/// Parse the next bytes of the Swift compiler JSON output.
81+
/// - Note: If a parsing error is encountered, the delegate will be notified and the parser won't accept any further
82+
/// input.
83+
func parse<C>(bytes: C) where C: Collection, C.Element == UInt8 {
84+
guard !hasFailed else { return }
85+
86+
do {
87+
try parseImpl(bytes: bytes)
88+
} catch {
89+
hasFailed = true
90+
delegate.swiftCompilerOutputParserDidFail(withError: error)
91+
}
92+
}
93+
}
94+
95+
private extension SwiftCompilerOutputParser {
96+
97+
/// Error corresponding to invalid Swift compiler output.
98+
struct ParsingError: LocalizedError {
99+
/// Text describing the specific reason for the parsing failure.
100+
let reason: String
101+
102+
var errorDescription: String? {
103+
return reason
104+
}
105+
}
106+
107+
/// Throwing implementation of the parse function.
108+
func parseImpl<C>(bytes: C) throws where C: Collection, C.Element == UInt8 {
109+
switch state {
110+
case .parsingMessageSize:
111+
if let newlineIndex = bytes.index(of: newline) {
112+
buffer.append(contentsOf: bytes[..<newlineIndex])
113+
try parseMessageSize()
114+
115+
let nextIndex = bytes.index(after: newlineIndex)
116+
try parseImpl(bytes: bytes[nextIndex...])
117+
} else {
118+
buffer.append(contentsOf: bytes)
119+
}
120+
case .parsingMessage(size: let size):
121+
let remainingBytes = size - buffer.count
122+
if remainingBytes <= bytes.count {
123+
buffer.append(contentsOf: bytes.prefix(remainingBytes))
124+
125+
let message = try parseMessage()
126+
delegate.swiftCompilerDidOutputMessage(message)
127+
128+
if case .signalled = message.kind {
129+
hasFailed = true
130+
return
131+
}
132+
133+
try parseImpl(bytes: bytes.dropFirst(remainingBytes))
134+
} else {
135+
buffer.append(contentsOf: bytes)
136+
}
137+
case .parsingNewlineAfterMessage:
138+
if let firstByte = bytes.first {
139+
precondition(firstByte == newline)
140+
state = .parsingMessageSize
141+
try parseImpl(bytes: bytes.dropFirst())
142+
}
143+
}
144+
}
145+
146+
/// Parse the next message size from the buffer and update the state machine.
147+
func parseMessageSize() throws {
148+
guard let string = String(bytes: buffer, encoding: .utf8) else {
149+
throw ParsingError(reason: "invalid UTF8 bytes")
150+
}
151+
152+
guard let messageSize = Int(string) else {
153+
throw ParsingError(reason: "invalid message size")
154+
}
155+
156+
buffer.removeAll()
157+
state = .parsingMessage(size: messageSize)
158+
}
159+
160+
/// Parse the message in the buffer and update the state machine.
161+
func parseMessage() throws -> SwiftCompilerMessage {
162+
let data = Data(bytes: buffer)
163+
buffer.removeAll()
164+
state = .parsingNewlineAfterMessage
165+
166+
do {
167+
return try decoder.decode(SwiftCompilerMessage.self, from: data)
168+
} catch {
169+
throw ParsingError(reason: "unexpected JSON message")
170+
}
171+
}
172+
}
173+
174+
extension SwiftCompilerMessage: Decodable, Equatable {
175+
enum CodingKeys: CodingKey {
176+
case pid
177+
case name
178+
}
179+
180+
init(from decoder: Decoder) throws {
181+
let container = try decoder.container(keyedBy: CodingKeys.self)
182+
name = try container.decode(String.self, forKey: .name)
183+
kind = try Kind(from: decoder)
184+
}
185+
}
186+
187+
extension SwiftCompilerMessage.Kind: Decodable, Equatable {
188+
enum CodingKeys: CodingKey {
189+
case kind
190+
}
191+
192+
init(from decoder: Decoder) throws {
193+
let container = try decoder.container(keyedBy: CodingKeys.self)
194+
let kind = try container.decode(String.self, forKey: .kind)
195+
switch kind {
196+
case "began":
197+
self = try .began(CommandInfo(from: decoder))
198+
case "skipped":
199+
self = try .skipped(CommandInfo(from: decoder))
200+
case "finished":
201+
self = try .finished(OutputInfo(from: decoder))
202+
case "signalled":
203+
self = try .signalled(OutputInfo(from: decoder))
204+
default:
205+
throw DecodingError.dataCorruptedError(forKey: .kind, in: container, debugDescription: "invalid kind")
206+
}
207+
}
208+
}
209+
210+
extension SwiftCompilerMessage.Kind.Output: Decodable, Equatable {}
211+
extension SwiftCompilerMessage.Kind.CommandInfo: Decodable, Equatable {}
212+
extension SwiftCompilerMessage.Kind.OutputInfo: Decodable, Equatable {}
213+
214+
private let newline = UInt8(ascii: "\n")

0 commit comments

Comments
 (0)