prefix-safe-json 0.0.1-alpha.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +72 -33
- package/package.json +8 -3
- package/dist/coordinator/coordinator.d.ts +0 -53
- package/dist/coordinator/coordinator.d.ts.map +0 -1
- package/dist/coordinator/coordinator.js +0 -448
- package/dist/coordinator/coordinator.js.map +0 -1
- package/dist/coordinator/diagnostic-codes.d.ts +0 -11
- package/dist/coordinator/diagnostic-codes.d.ts.map +0 -1
- package/dist/coordinator/diagnostic-codes.js +0 -22
- package/dist/coordinator/diagnostic-codes.js.map +0 -1
- package/dist/coordinator/protocol.d.ts +0 -61
- package/dist/coordinator/protocol.d.ts.map +0 -1
- package/dist/coordinator/protocol.js +0 -2
- package/dist/coordinator/protocol.js.map +0 -1
- package/dist/coordinator/types.d.ts +0 -104
- package/dist/coordinator/types.d.ts.map +0 -1
- package/dist/coordinator/types.js +0 -7
- package/dist/coordinator/types.js.map +0 -1
- package/dist/diagnostics/codes.d.ts +0 -24
- package/dist/diagnostics/codes.d.ts.map +0 -1
- package/dist/diagnostics/codes.js +0 -28
- package/dist/diagnostics/codes.js.map +0 -1
- package/dist/diagnostics/factory.d.ts +0 -8
- package/dist/diagnostics/factory.d.ts.map +0 -1
- package/dist/diagnostics/factory.js +0 -17
- package/dist/diagnostics/factory.js.map +0 -1
- package/dist/gate/decide.d.ts +0 -20
- package/dist/gate/decide.d.ts.map +0 -1
- package/dist/gate/decide.js +0 -127
- package/dist/gate/decide.js.map +0 -1
- package/dist/gate/gate.d.ts +0 -25
- package/dist/gate/gate.d.ts.map +0 -1
- package/dist/gate/gate.js +0 -80
- package/dist/gate/gate.js.map +0 -1
- package/dist/gate/types.d.ts +0 -122
- package/dist/gate/types.d.ts.map +0 -1
- package/dist/gate/types.js +0 -2
- package/dist/gate/types.js.map +0 -1
- package/dist/grammar/frame.d.ts +0 -58
- package/dist/grammar/frame.d.ts.map +0 -1
- package/dist/grammar/frame.js +0 -87
- package/dist/grammar/frame.js.map +0 -1
- package/dist/grammar/pointer.d.ts +0 -10
- package/dist/grammar/pointer.d.ts.map +0 -1
- package/dist/grammar/pointer.js +0 -20
- package/dist/grammar/pointer.js.map +0 -1
- package/dist/grammar/stack.d.ts +0 -73
- package/dist/grammar/stack.d.ts.map +0 -1
- package/dist/grammar/stack.js +0 -168
- package/dist/grammar/stack.js.map +0 -1
- package/dist/index.d.ts +0 -37
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -37
- package/dist/index.js.map +0 -1
- package/dist/lexer/scanner.d.ts +0 -108
- package/dist/lexer/scanner.d.ts.map +0 -1
- package/dist/lexer/scanner.js +0 -676
- package/dist/lexer/scanner.js.map +0 -1
- package/dist/lexer/states.d.ts +0 -35
- package/dist/lexer/states.d.ts.map +0 -1
- package/dist/lexer/states.js +0 -39
- package/dist/lexer/states.js.map +0 -1
- package/dist/lexer/tokens.d.ts +0 -22
- package/dist/lexer/tokens.d.ts.map +0 -1
- package/dist/lexer/tokens.js +0 -22
- package/dist/lexer/tokens.js.map +0 -1
- package/dist/limits.d.ts +0 -14
- package/dist/limits.d.ts.map +0 -1
- package/dist/limits.js +0 -23
- package/dist/limits.js.map +0 -1
- package/dist/parser.d.ts +0 -6
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -949
- package/dist/parser.js.map +0 -1
- package/dist/providers/adapter.d.ts +0 -10
- package/dist/providers/adapter.d.ts.map +0 -1
- package/dist/providers/adapter.js +0 -2
- package/dist/providers/adapter.js.map +0 -1
- package/dist/providers/ai-sdk.d.ts +0 -54
- package/dist/providers/ai-sdk.d.ts.map +0 -1
- package/dist/providers/ai-sdk.js +0 -191
- package/dist/providers/ai-sdk.js.map +0 -1
- package/dist/providers/anthropic.d.ts +0 -45
- package/dist/providers/anthropic.d.ts.map +0 -1
- package/dist/providers/anthropic.js +0 -112
- package/dist/providers/anthropic.js.map +0 -1
- package/dist/providers/gemini.d.ts +0 -31
- package/dist/providers/gemini.d.ts.map +0 -1
- package/dist/providers/gemini.js +0 -94
- package/dist/providers/gemini.js.map +0 -1
- package/dist/providers/openai-compatible.d.ts +0 -33
- package/dist/providers/openai-compatible.d.ts.map +0 -1
- package/dist/providers/openai-compatible.js +0 -160
- package/dist/providers/openai-compatible.js.map +0 -1
- package/dist/providers/openai.d.ts +0 -48
- package/dist/providers/openai.d.ts.map +0 -1
- package/dist/providers/openai.js +0 -205
- package/dist/providers/openai.js.map +0 -1
- package/dist/providers/openrouter.d.ts +0 -35
- package/dist/providers/openrouter.d.ts.map +0 -1
- package/dist/providers/openrouter.js +0 -55
- package/dist/providers/openrouter.js.map +0 -1
- package/dist/semantic/builder.d.ts +0 -55
- package/dist/semantic/builder.d.ts.map +0 -1
- package/dist/semantic/builder.js +0 -140
- package/dist/semantic/builder.js.map +0 -1
- package/dist/semantic/events.d.ts +0 -2
- package/dist/semantic/events.d.ts.map +0 -1
- package/dist/semantic/events.js +0 -5
- package/dist/semantic/events.js.map +0 -1
- package/dist/semantic/snapshot.d.ts +0 -49
- package/dist/semantic/snapshot.d.ts.map +0 -1
- package/dist/semantic/snapshot.js +0 -228
- package/dist/semantic/snapshot.js.map +0 -1
- package/dist/types.d.ts +0 -156
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/utf8/decoder.d.ts +0 -53
- package/dist/utf8/decoder.d.ts.map +0 -1
- package/dist/utf8/decoder.js +0 -323
- package/dist/utf8/decoder.js.map +0 -1
package/dist/parser.js
DELETED
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// IncrementalJsonParser — main parser implementation
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
//
|
|
5
|
-
// Integrates: UTF-8 decoder → Lexer/Scanner → Grammar Stack → Event Builder
|
|
6
|
-
//
|
|
7
|
-
// Public API: push(), snapshot(), drainEvents(), finish()
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
import { DiagnosticCode } from "./diagnostics/codes.js";
|
|
10
|
-
import { DEFAULT_LIMITS, sanitizeLimit } from "./limits.js";
|
|
11
|
-
import { Utf8Decoder, stringToUtf8 } from "./utf8/decoder.js";
|
|
12
|
-
import { Scanner } from "./lexer/scanner.js";
|
|
13
|
-
import { ScannerState } from "./lexer/states.js";
|
|
14
|
-
import { TokenType } from "./lexer/tokens.js";
|
|
15
|
-
import { GrammarStack } from "./grammar/stack.js";
|
|
16
|
-
import { EventBuilder } from "./semantic/builder.js";
|
|
17
|
-
import { SnapshotBuilder } from "./semantic/snapshot.js";
|
|
18
|
-
import { createDiagnostic } from "./diagnostics/factory.js";
|
|
19
|
-
/**
|
|
20
|
-
* Create a new IncrementalJsonParser.
|
|
21
|
-
*/
|
|
22
|
-
export function createParser(options) {
|
|
23
|
-
return new Parser(options);
|
|
24
|
-
}
|
|
25
|
-
class Parser {
|
|
26
|
-
limits;
|
|
27
|
-
utf8;
|
|
28
|
-
scanner;
|
|
29
|
-
grammar;
|
|
30
|
-
events;
|
|
31
|
-
snapshot_;
|
|
32
|
-
allDiagnostics = [];
|
|
33
|
-
allRepairs = [];
|
|
34
|
-
// Sticky flags mirroring the *existence* of certain diagnostics/repairs,
|
|
35
|
-
// set unconditionally in addDiagnostic()/addRepair() regardless of
|
|
36
|
-
// whether the corresponding array is at its maxQueuedEvents cap. Several
|
|
37
|
-
// checks (isExecutable, determineOutcome) need an exact, monotonic answer
|
|
38
|
-
// to "did X ever happen for this stream" — re-deriving that by scanning
|
|
39
|
-
// allDiagnostics/allRepairs is unsound once those arrays can be truncated,
|
|
40
|
-
// since a disqualifying entry could be the one silently dropped. This is
|
|
41
|
-
// the same reasoning that already made grammar.hasDuplicate a dedicated
|
|
42
|
-
// boolean instead of an array scan.
|
|
43
|
-
everHadFatalDiagnostic = false;
|
|
44
|
-
everHadNonRecoverableError = false;
|
|
45
|
-
everHadStructuralOrLossyRepair = false;
|
|
46
|
-
everHadCloseContainerRepair = false;
|
|
47
|
-
phase = "collecting";
|
|
48
|
-
receivedBytes = 0;
|
|
49
|
-
consumedBytes = 0;
|
|
50
|
-
rootComplete = false;
|
|
51
|
-
terminal = false;
|
|
52
|
-
hasSeenValue = false;
|
|
53
|
-
syntax_ = "empty";
|
|
54
|
-
trailingDataSeen = false;
|
|
55
|
-
trailingDataBytes = 0;
|
|
56
|
-
rootScalarValue = undefined;
|
|
57
|
-
repairs;
|
|
58
|
-
// Track if we're in a duplicate-key value (to skip it)
|
|
59
|
-
skipValueDepth = 0;
|
|
60
|
-
isSkippingValue = false;
|
|
61
|
-
// For finish
|
|
62
|
-
finishMeta;
|
|
63
|
-
constructor(options) {
|
|
64
|
-
const userLimits = options?.limits ?? {};
|
|
65
|
-
this.limits = {
|
|
66
|
-
maxInputBytes: sanitizeLimit(userLimits.maxInputBytes, DEFAULT_LIMITS.maxInputBytes),
|
|
67
|
-
maxDepth: sanitizeLimit(userLimits.maxDepth, DEFAULT_LIMITS.maxDepth),
|
|
68
|
-
maxStringBytes: sanitizeLimit(userLimits.maxStringBytes, DEFAULT_LIMITS.maxStringBytes),
|
|
69
|
-
maxQueuedEvents: sanitizeLimit(userLimits.maxQueuedEvents, DEFAULT_LIMITS.maxQueuedEvents),
|
|
70
|
-
maxTrailingDataBytes: sanitizeLimit(userLimits.maxTrailingDataBytes, DEFAULT_LIMITS.maxTrailingDataBytes),
|
|
71
|
-
};
|
|
72
|
-
this.repairs = {
|
|
73
|
-
rawControlCharacters: options?.repairs?.rawControlCharacters ?? "reject",
|
|
74
|
-
trailingData: options?.repairs?.trailingData ?? "isolate",
|
|
75
|
-
closeContainersAtFinish: options?.repairs?.closeContainersAtFinish ?? "disabled",
|
|
76
|
-
};
|
|
77
|
-
this.utf8 = new Utf8Decoder();
|
|
78
|
-
this.scanner = new Scanner(options);
|
|
79
|
-
this.grammar = new GrammarStack(this.limits.maxDepth);
|
|
80
|
-
this.events = new EventBuilder(this.limits.maxQueuedEvents);
|
|
81
|
-
this.snapshot_ = new SnapshotBuilder();
|
|
82
|
-
}
|
|
83
|
-
push(chunk) {
|
|
84
|
-
if (this.phase === "finished") {
|
|
85
|
-
throw new Error("E_PUSH_AFTER_FINISH: push() called after finish()");
|
|
86
|
-
}
|
|
87
|
-
if (this.terminal) {
|
|
88
|
-
return {
|
|
89
|
-
acceptedBytes: 0,
|
|
90
|
-
emittedEvents: 0,
|
|
91
|
-
syntax: this.syntax_,
|
|
92
|
-
terminal: true,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// Convert string to bytes
|
|
96
|
-
const bytes = typeof chunk === "string" ? stringToUtf8(chunk) : chunk;
|
|
97
|
-
// Check input limit
|
|
98
|
-
if (this.receivedBytes + bytes.length > this.limits.maxInputBytes) {
|
|
99
|
-
const diag = createDiagnostic(DiagnosticCode.E_LIMIT_INPUT_BYTES, "fatal", this.receivedBytes, `Input exceeds maximum of ${this.limits.maxInputBytes} bytes`, false);
|
|
100
|
-
this.addDiagnostic(diag);
|
|
101
|
-
this.terminal = true;
|
|
102
|
-
this.syntax_ = "invalid";
|
|
103
|
-
return {
|
|
104
|
-
acceptedBytes: 0,
|
|
105
|
-
emittedEvents: 0,
|
|
106
|
-
syntax: this.syntax_,
|
|
107
|
-
terminal: true,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
this.receivedBytes += bytes.length;
|
|
111
|
-
this.events.resetPushCount();
|
|
112
|
-
// Decode UTF-8
|
|
113
|
-
const decoded = this.utf8.decode(bytes);
|
|
114
|
-
if (decoded.strippedBom) {
|
|
115
|
-
const repair = {
|
|
116
|
-
code: "R_STRIP_UTF8_BOM",
|
|
117
|
-
byteRange: [0, 3],
|
|
118
|
-
impact: "representation_preserving",
|
|
119
|
-
description: "Stripped UTF-8 BOM at index 0",
|
|
120
|
-
};
|
|
121
|
-
this.addRepair(repair);
|
|
122
|
-
}
|
|
123
|
-
// Handle UTF-8 errors
|
|
124
|
-
for (const err of decoded.errors) {
|
|
125
|
-
const code = err.kind === "overlong"
|
|
126
|
-
? DiagnosticCode.E_INVALID_UTF8
|
|
127
|
-
: err.kind === "out_of_range"
|
|
128
|
-
? DiagnosticCode.E_INVALID_UTF8
|
|
129
|
-
: err.kind === "invalid_start_byte"
|
|
130
|
-
? DiagnosticCode.E_INVALID_UTF8
|
|
131
|
-
: DiagnosticCode.E_INVALID_UTF8;
|
|
132
|
-
const diag = createDiagnostic(code, "error", err.byteOffset, `Invalid UTF-8: ${err.kind} at byte ${err.byteOffset}`, false);
|
|
133
|
-
this.addDiagnostic(diag);
|
|
134
|
-
this.syntax_ = "invalid";
|
|
135
|
-
this.terminal = true;
|
|
136
|
-
return {
|
|
137
|
-
acceptedBytes: decoded.consumed,
|
|
138
|
-
emittedEvents: this.events.emittedDuringPush,
|
|
139
|
-
syntax: this.syntax_,
|
|
140
|
-
terminal: true,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
// Feed decoded text to scanner character by character
|
|
144
|
-
const text = decoded.text;
|
|
145
|
-
// Track byte positions via UTF-8 encoding
|
|
146
|
-
let bytePos = this.consumedBytes;
|
|
147
|
-
for (const ch of text) {
|
|
148
|
-
if (this.terminal)
|
|
149
|
-
break;
|
|
150
|
-
const charBytes = getUtf8ByteLength(ch);
|
|
151
|
-
// If root is already complete, handle trailing content at parser level
|
|
152
|
-
if (this.rootComplete) {
|
|
153
|
-
const isWs = ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
|
|
154
|
-
if (!isWs) {
|
|
155
|
-
if (!this.trailingDataSeen) {
|
|
156
|
-
this.trailingDataSeen = true;
|
|
157
|
-
if (this.repairs.trailingData === "reject") {
|
|
158
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_TRAILING_DATA, "error", bytePos, `Trailing data rejected`, false));
|
|
159
|
-
this.syntax_ = "invalid";
|
|
160
|
-
this.terminal = true;
|
|
161
|
-
return {
|
|
162
|
-
acceptedBytes: decoded.consumed,
|
|
163
|
-
emittedEvents: this.events.emittedDuringPush,
|
|
164
|
-
syntax: this.syntax_,
|
|
165
|
-
terminal: true,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_TRAILING_DATA, "warning", bytePos, `Unexpected data after root JSON value`, true));
|
|
169
|
-
const repair = {
|
|
170
|
-
code: "R_ISOLATE_TRAILING_DATA",
|
|
171
|
-
byteRange: [bytePos, bytePos + charBytes],
|
|
172
|
-
impact: "root_preserving",
|
|
173
|
-
description: "Isolated trailing data after complete JSON root",
|
|
174
|
-
};
|
|
175
|
-
this.addRepair(repair);
|
|
176
|
-
}
|
|
177
|
-
this.trailingDataBytes += charBytes;
|
|
178
|
-
if (this.trailingDataBytes > this.limits.maxTrailingDataBytes) {
|
|
179
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_LIMIT_INPUT_BYTES, "error", bytePos, `Maximum trailing data bytes exceeded`, false));
|
|
180
|
-
this.syntax_ = "invalid";
|
|
181
|
-
this.terminal = true;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
bytePos += charBytes;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
// Set up scanner for object key context
|
|
188
|
-
const frame = this.grammar.current;
|
|
189
|
-
if (frame &&
|
|
190
|
-
frame.containerType === "object" &&
|
|
191
|
-
(frame.objectExpectation === "first_key_or_end" || frame.objectExpectation === "key_after_comma") &&
|
|
192
|
-
this.scanner.currentState === ScannerState.Structural) {
|
|
193
|
-
this.scanner.setNextStringIsKey(true);
|
|
194
|
-
}
|
|
195
|
-
this.scanner.feedChar(ch, bytePos, charBytes);
|
|
196
|
-
// Process any emitted tokens
|
|
197
|
-
const tokens = this.scanner.takeTokens();
|
|
198
|
-
for (const token of tokens) {
|
|
199
|
-
if (this.terminal)
|
|
200
|
-
break;
|
|
201
|
-
this.processToken(token);
|
|
202
|
-
}
|
|
203
|
-
// Process any scanner diagnostics and repairs
|
|
204
|
-
const scanDiags = this.scanner.takeDiagnostics();
|
|
205
|
-
const scanRepairs = this.scanner.takeRepairs();
|
|
206
|
-
for (const repair of scanRepairs) {
|
|
207
|
-
this.addRepair(repair);
|
|
208
|
-
}
|
|
209
|
-
for (const diag of scanDiags) {
|
|
210
|
-
this.addDiagnostic(diag);
|
|
211
|
-
if (diag.severity === "fatal" ||
|
|
212
|
-
(diag.severity === "error" && !diag.recoverable)) {
|
|
213
|
-
this.syntax_ = "invalid";
|
|
214
|
-
this.terminal = true;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
// Process grammar diagnostics
|
|
218
|
-
const grammarDiags = this.grammar.takeDiagnostics();
|
|
219
|
-
for (const diag of grammarDiags) {
|
|
220
|
-
this.addDiagnostic(diag);
|
|
221
|
-
if (diag.severity === "fatal") {
|
|
222
|
-
this.terminal = true;
|
|
223
|
-
this.syntax_ = "invalid";
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (this.events.isTerminal) {
|
|
227
|
-
this.terminal = true;
|
|
228
|
-
this.syntax_ = "invalid";
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
bytePos += charBytes;
|
|
232
|
-
}
|
|
233
|
-
this.consumedBytes = bytePos;
|
|
234
|
-
return {
|
|
235
|
-
acceptedBytes: decoded.consumed,
|
|
236
|
-
emittedEvents: this.events.emittedDuringPush,
|
|
237
|
-
syntax: this.syntax_,
|
|
238
|
-
terminal: this.terminal,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
snapshot() {
|
|
242
|
-
const pending = this.buildPendingTokens();
|
|
243
|
-
return {
|
|
244
|
-
phase: this.phase,
|
|
245
|
-
syntax: this.syntax_,
|
|
246
|
-
stableValue: this.buildStableValue(),
|
|
247
|
-
rootComplete: this.rootComplete,
|
|
248
|
-
executable: this.isExecutable(),
|
|
249
|
-
pending,
|
|
250
|
-
repairs: Object.freeze([...this.allRepairs]),
|
|
251
|
-
diagnostics: Object.freeze([...this.allDiagnostics]),
|
|
252
|
-
receivedBytes: this.receivedBytes,
|
|
253
|
-
consumedBytes: this.consumedBytes,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
drainEvents() {
|
|
257
|
-
return this.events.drain();
|
|
258
|
-
}
|
|
259
|
-
finish(meta) {
|
|
260
|
-
if (this.phase === "finished") {
|
|
261
|
-
throw new Error("E_PUSH_AFTER_FINISH: finish() called more than once");
|
|
262
|
-
}
|
|
263
|
-
this.phase = "finished";
|
|
264
|
-
this.finishMeta = meta;
|
|
265
|
-
const reason = meta?.reason ?? "unknown";
|
|
266
|
-
// Try to finalize any pending number or literal at stream end
|
|
267
|
-
if (!this.terminal) {
|
|
268
|
-
// Check for pending number — at stream end, numbers can be finalized
|
|
269
|
-
// only if the stream completed normally
|
|
270
|
-
const pendingInfo = this.scanner.getPendingInfo();
|
|
271
|
-
if (pendingInfo.type === "number") {
|
|
272
|
-
if (reason === "complete") {
|
|
273
|
-
// Stream ended normally — the number terminator is implicit EOF
|
|
274
|
-
const finalized = this.scanner.finalizeNumber();
|
|
275
|
-
if (finalized === "finalized") {
|
|
276
|
-
const tokens = this.scanner.takeTokens();
|
|
277
|
-
for (const token of tokens) {
|
|
278
|
-
this.processToken(token);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
else if (finalized === "invalid") {
|
|
282
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", pendingInfo.byteStart, `Invalid number at stream end: ${pendingInfo.buffer}`, false));
|
|
283
|
-
// Unlike every other error branch in this method, this one
|
|
284
|
-
// never routed through commitScalar()/the scanner's own
|
|
285
|
-
// Invalid-state transition, so nothing else here sets
|
|
286
|
-
// terminal/syntax_. Without this, a definitively malformed
|
|
287
|
-
// number (leading zero, trailing "."/exponent - not fixable by
|
|
288
|
-
// more input, unlike a genuine truncation) fell through
|
|
289
|
-
// determineOutcome()'s generic `!rootComplete` case and was
|
|
290
|
-
// misreported as outcome "truncated" instead of "invalid" for
|
|
291
|
-
// a bare top-level root (e.g. plain "01" or "-01" as the whole
|
|
292
|
-
// document) - the same malformation wrapped in a container
|
|
293
|
-
// (e.g. `{"a":01}`) already reported "invalid" correctly via
|
|
294
|
-
// the scanner's ScannerState.Invalid path.
|
|
295
|
-
this.terminal = true;
|
|
296
|
-
this.syntax_ = "invalid";
|
|
297
|
-
}
|
|
298
|
-
// "not_ready": scanner wasn't in a finalizable number state at all
|
|
299
|
-
// (e.g. buffer is just "-", or ends right at a bare "e"/"E") —
|
|
300
|
-
// preserved exactly as before: no diagnostic added here.
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// Truncated — number is incomplete
|
|
304
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_NUMBER, "error", pendingInfo.byteStart, `Incomplete number at stream end: ${pendingInfo.buffer}`, false));
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
else if (pendingInfo.type === "literal") {
|
|
308
|
-
if (reason === "complete" &&
|
|
309
|
-
pendingInfo.buffer.length ===
|
|
310
|
-
(pendingInfo.buffer === "tru" || pendingInfo.buffer === "fal"
|
|
311
|
-
? 0
|
|
312
|
-
: pendingInfo.buffer.length)) {
|
|
313
|
-
const finalized = this.scanner.finalizeLiteral();
|
|
314
|
-
if (finalized) {
|
|
315
|
-
const tokens = this.scanner.takeTokens();
|
|
316
|
-
for (const token of tokens) {
|
|
317
|
-
this.processToken(token);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_LITERAL, "error", pendingInfo.byteStart, `Incomplete literal at stream end: ${pendingInfo.buffer}`, false));
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_LITERAL, "error", pendingInfo.byteStart, `Incomplete literal at stream end: ${pendingInfo.buffer}`, false));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else if (pendingInfo.type === "string" ||
|
|
329
|
-
pendingInfo.type === "object_key") {
|
|
330
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNTERMINATED_STRING, "error", pendingInfo.byteStart, "Unterminated string at stream end", false));
|
|
331
|
-
}
|
|
332
|
-
else if (pendingInfo.type === "unicode_escape") {
|
|
333
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_UNICODE_ESCAPE, "error", pendingInfo.byteStart, "Incomplete unicode escape at stream end", false));
|
|
334
|
-
}
|
|
335
|
-
else if (pendingInfo.type === "surrogate") {
|
|
336
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNPAIRED_SURROGATE, "error", pendingInfo.byteStart, "Unpaired surrogate at stream end", false));
|
|
337
|
-
}
|
|
338
|
-
// Check for incomplete UTF-8
|
|
339
|
-
if (this.utf8.hasIncomplete()) {
|
|
340
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_UTF8, "error", this.utf8.incompleteByteOffset(), "Incomplete UTF-8 sequence at stream end", false));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// Check for truncation
|
|
344
|
-
if (!this.rootComplete &&
|
|
345
|
-
reason !== "complete") {
|
|
346
|
-
// Attempt structural salvage
|
|
347
|
-
let salvaged = false;
|
|
348
|
-
if (this.repairs.closeContainersAtFinish === "safe-only" &&
|
|
349
|
-
this.scanner.currentState === ScannerState.Structural &&
|
|
350
|
-
this.scanner.getPendingInfo().type === "none" &&
|
|
351
|
-
this.grammar.canSafelyCloseAll() &&
|
|
352
|
-
!this.hasFatalDiagnostic() &&
|
|
353
|
-
(this.syntax_ !== "invalid")) {
|
|
354
|
-
salvaged = true;
|
|
355
|
-
const frames = this.grammar.getFrames();
|
|
356
|
-
// We know exactly what to close, just close them all
|
|
357
|
-
for (let i = frames.length - 1; i >= 0; i--) {
|
|
358
|
-
const frame = frames[i];
|
|
359
|
-
if (frame) {
|
|
360
|
-
this.events.emitContainerClosed(frame.path, frame.containerType);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const repair = {
|
|
364
|
-
code: "R_CLOSE_CONTAINER",
|
|
365
|
-
byteRange: [this.consumedBytes, this.consumedBytes],
|
|
366
|
-
impact: "structural",
|
|
367
|
-
description: `Safely closed ${frames.length} containers at stream end`,
|
|
368
|
-
};
|
|
369
|
-
this.addRepair(repair);
|
|
370
|
-
this.rootComplete = true;
|
|
371
|
-
}
|
|
372
|
-
if (!salvaged) {
|
|
373
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_STREAM_TRUNCATED, "error", this.consumedBytes, `Stream truncated: ${reason}`, false));
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
else if (!this.rootComplete && reason === "complete") {
|
|
377
|
-
// Stream said complete but root isn't closed
|
|
378
|
-
if (this.hasSeenValue) {
|
|
379
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_STREAM_TRUNCATED, "error", this.consumedBytes, "Stream marked complete but JSON root is not closed", false));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const outcome = this.determineOutcome(reason);
|
|
383
|
-
const executable = this.isExecutable();
|
|
384
|
-
// Emit finish events
|
|
385
|
-
if (this.rootComplete && !this.grammar.hasDuplicate && !this.hasFatalDiagnostic()) {
|
|
386
|
-
this.events.emitDocumentComplete(executable);
|
|
387
|
-
}
|
|
388
|
-
this.events.emitStreamFinished(outcome);
|
|
389
|
-
return {
|
|
390
|
-
syntax: this.syntax_,
|
|
391
|
-
outcome,
|
|
392
|
-
stableValue: this.buildStableValue(),
|
|
393
|
-
executable,
|
|
394
|
-
repairs: Object.freeze([...this.allRepairs]),
|
|
395
|
-
diagnostics: Object.freeze([...this.allDiagnostics]),
|
|
396
|
-
receivedBytes: this.receivedBytes,
|
|
397
|
-
consumedBytes: this.consumedBytes,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
// -----------------------------------------------------------------------
|
|
401
|
-
// Token processing
|
|
402
|
-
// -----------------------------------------------------------------------
|
|
403
|
-
processToken(token) {
|
|
404
|
-
// Handle value skipping (duplicate key value)
|
|
405
|
-
if (this.isSkippingValue) {
|
|
406
|
-
if (token.type === TokenType.ObjectStart ||
|
|
407
|
-
token.type === TokenType.ArrayStart) {
|
|
408
|
-
this.skipValueDepth++;
|
|
409
|
-
}
|
|
410
|
-
else if (token.type === TokenType.ObjectEnd ||
|
|
411
|
-
token.type === TokenType.ArrayEnd) {
|
|
412
|
-
this.skipValueDepth--;
|
|
413
|
-
if (this.skipValueDepth <= 0) {
|
|
414
|
-
this.isSkippingValue = false;
|
|
415
|
-
this.skipValueDepth = 0;
|
|
416
|
-
// Resume normal flow — need to advance the object expectation
|
|
417
|
-
const frame = this.grammar.current;
|
|
418
|
-
if (frame && frame.containerType === "object") {
|
|
419
|
-
frame.objectExpectation = "comma_or_end";
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
else if (this.skipValueDepth === 0) {
|
|
424
|
-
// Scalar value for duplicate key — skip it
|
|
425
|
-
if (token.type === TokenType.String ||
|
|
426
|
-
token.type === TokenType.Number ||
|
|
427
|
-
token.type === TokenType.True ||
|
|
428
|
-
token.type === TokenType.False ||
|
|
429
|
-
token.type === TokenType.Null) {
|
|
430
|
-
this.isSkippingValue = false;
|
|
431
|
-
const frame = this.grammar.current;
|
|
432
|
-
if (frame && frame.containerType === "object") {
|
|
433
|
-
frame.objectExpectation = "comma_or_end";
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
switch (token.type) {
|
|
440
|
-
case TokenType.ObjectStart:
|
|
441
|
-
this.handleObjectStart(token);
|
|
442
|
-
break;
|
|
443
|
-
case TokenType.ObjectEnd:
|
|
444
|
-
this.handleObjectEnd(token);
|
|
445
|
-
break;
|
|
446
|
-
case TokenType.ArrayStart:
|
|
447
|
-
this.handleArrayStart(token);
|
|
448
|
-
break;
|
|
449
|
-
case TokenType.ArrayEnd:
|
|
450
|
-
this.handleArrayEnd(token);
|
|
451
|
-
break;
|
|
452
|
-
case TokenType.Colon:
|
|
453
|
-
this.handleColon(token);
|
|
454
|
-
break;
|
|
455
|
-
case TokenType.Comma:
|
|
456
|
-
this.handleComma(token);
|
|
457
|
-
break;
|
|
458
|
-
case TokenType.String:
|
|
459
|
-
this.handleString(token);
|
|
460
|
-
break;
|
|
461
|
-
case TokenType.Number:
|
|
462
|
-
this.handleNumber(token);
|
|
463
|
-
break;
|
|
464
|
-
case TokenType.True:
|
|
465
|
-
case TokenType.False:
|
|
466
|
-
case TokenType.Null:
|
|
467
|
-
this.handleLiteral(token);
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
handleObjectStart(token) {
|
|
472
|
-
const frame = this.grammar.current;
|
|
473
|
-
// Validate context
|
|
474
|
-
if (frame) {
|
|
475
|
-
if (frame.containerType === "object") {
|
|
476
|
-
if (frame.objectExpectation !== "value") {
|
|
477
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '{'", false));
|
|
478
|
-
this.terminal = true;
|
|
479
|
-
this.syntax_ = "invalid";
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
else if (frame.containerType === "array") {
|
|
484
|
-
if (frame.arrayExpectation !== "first_value_or_end" &&
|
|
485
|
-
frame.arrayExpectation !== "value_after_comma") {
|
|
486
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '{' in array", false));
|
|
487
|
-
this.terminal = true;
|
|
488
|
-
this.syntax_ = "invalid";
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
const newFrame = this.grammar.pushObject(token.byteStart);
|
|
494
|
-
if (!newFrame) {
|
|
495
|
-
this.terminal = true;
|
|
496
|
-
this.syntax_ = "invalid";
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
this.hasSeenValue = true;
|
|
500
|
-
if (this.syntax_ === "empty") {
|
|
501
|
-
this.syntax_ = "incomplete";
|
|
502
|
-
}
|
|
503
|
-
// Init container in snapshot builder
|
|
504
|
-
if (!this.grammar.isEmpty()) {
|
|
505
|
-
if (this.grammar.depth === 1) {
|
|
506
|
-
this.snapshot_.initRootObject(newFrame);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
this.snapshot_.initContainerForFrame(newFrame, "object");
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
handleObjectEnd(token) {
|
|
514
|
-
const frame = this.grammar.current;
|
|
515
|
-
if (!frame || frame.containerType !== "object") {
|
|
516
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '}' — no matching open object", false));
|
|
517
|
-
this.terminal = true;
|
|
518
|
-
this.syntax_ = "invalid";
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (frame.objectExpectation !== "first_key_or_end" &&
|
|
522
|
-
frame.objectExpectation !== "comma_or_end") {
|
|
523
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, `Unexpected '}' while expecting ${frame.objectExpectation}`, false));
|
|
524
|
-
this.terminal = true;
|
|
525
|
-
this.syntax_ = "invalid";
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
this.grammar.pop();
|
|
529
|
-
this.events.emitContainerClosed(frame.path, "object");
|
|
530
|
-
// If this closes the root, emit the full object as committed
|
|
531
|
-
if (this.grammar.isEmpty()) {
|
|
532
|
-
this.rootComplete = true;
|
|
533
|
-
this.syntax_ = "root_complete";
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
// Value committed for parent
|
|
537
|
-
this.commitContainerToParent(frame.path, token);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
handleArrayStart(token) {
|
|
541
|
-
const frame = this.grammar.current;
|
|
542
|
-
// Validate context
|
|
543
|
-
if (frame) {
|
|
544
|
-
if (frame.containerType === "object") {
|
|
545
|
-
if (frame.objectExpectation !== "value") {
|
|
546
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '['", false));
|
|
547
|
-
this.terminal = true;
|
|
548
|
-
this.syntax_ = "invalid";
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
else if (frame.containerType === "array") {
|
|
553
|
-
if (frame.arrayExpectation !== "first_value_or_end" &&
|
|
554
|
-
frame.arrayExpectation !== "value_after_comma") {
|
|
555
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '[' in array", false));
|
|
556
|
-
this.terminal = true;
|
|
557
|
-
this.syntax_ = "invalid";
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const newFrame = this.grammar.pushArray(token.byteStart);
|
|
563
|
-
if (!newFrame) {
|
|
564
|
-
this.terminal = true;
|
|
565
|
-
this.syntax_ = "invalid";
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
this.hasSeenValue = true;
|
|
569
|
-
if (this.syntax_ === "empty") {
|
|
570
|
-
this.syntax_ = "incomplete";
|
|
571
|
-
}
|
|
572
|
-
// Init container in snapshot builder
|
|
573
|
-
if (this.grammar.depth === 1) {
|
|
574
|
-
this.snapshot_.initRootArray(newFrame);
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
this.snapshot_.initContainerForFrame(newFrame, "array");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
handleArrayEnd(token) {
|
|
581
|
-
const frame = this.grammar.current;
|
|
582
|
-
if (!frame || frame.containerType !== "array") {
|
|
583
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ']' — no matching open array", false));
|
|
584
|
-
this.terminal = true;
|
|
585
|
-
this.syntax_ = "invalid";
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (frame.arrayExpectation !== "first_value_or_end" &&
|
|
589
|
-
frame.arrayExpectation !== "comma_or_end") {
|
|
590
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ']'", false));
|
|
591
|
-
this.terminal = true;
|
|
592
|
-
this.syntax_ = "invalid";
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
this.grammar.pop();
|
|
596
|
-
this.events.emitContainerClosed(frame.path, "array");
|
|
597
|
-
if (this.grammar.isEmpty()) {
|
|
598
|
-
this.rootComplete = true;
|
|
599
|
-
this.syntax_ = "root_complete";
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
this.commitContainerToParent(frame.path, token);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
handleColon(token) {
|
|
606
|
-
const frame = this.grammar.current;
|
|
607
|
-
if (!frame || frame.containerType !== "object" || frame.objectExpectation !== "colon") {
|
|
608
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ':'", false));
|
|
609
|
-
this.terminal = true;
|
|
610
|
-
this.syntax_ = "invalid";
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
frame.objectExpectation = "value";
|
|
614
|
-
}
|
|
615
|
-
handleComma(token) {
|
|
616
|
-
const frame = this.grammar.current;
|
|
617
|
-
if (!frame) {
|
|
618
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ','", false));
|
|
619
|
-
this.terminal = true;
|
|
620
|
-
this.syntax_ = "invalid";
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (frame.containerType === "object") {
|
|
624
|
-
if (frame.objectExpectation !== "comma_or_end") {
|
|
625
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ',' in object", false));
|
|
626
|
-
this.terminal = true;
|
|
627
|
-
this.syntax_ = "invalid";
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
frame.objectExpectation = "key_after_comma";
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
if (frame.arrayExpectation !== "comma_or_end") {
|
|
634
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ',' in array", false));
|
|
635
|
-
this.terminal = true;
|
|
636
|
-
this.syntax_ = "invalid";
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
frame.arrayExpectation = "value_after_comma";
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
handleString(token) {
|
|
643
|
-
const frame = this.grammar.current;
|
|
644
|
-
if (!frame) {
|
|
645
|
-
// Root-level string value
|
|
646
|
-
this.hasSeenValue = true;
|
|
647
|
-
this.rootComplete = true;
|
|
648
|
-
this.syntax_ = "root_complete";
|
|
649
|
-
this.events.emitValueCommitted("", token.value, [
|
|
650
|
-
token.byteStart,
|
|
651
|
-
token.byteEnd,
|
|
652
|
-
]);
|
|
653
|
-
this.rootScalarValue = token.value;
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (frame.containerType === "object") {
|
|
657
|
-
if (frame.objectExpectation === "first_key_or_end" ||
|
|
658
|
-
frame.objectExpectation === "key_after_comma") {
|
|
659
|
-
// This is an object key
|
|
660
|
-
const isNew = this.grammar.registerObjectKey(token.value, token.byteStart);
|
|
661
|
-
if (!isNew) {
|
|
662
|
-
// Duplicate key — skip the upcoming value
|
|
663
|
-
// The grammar registered the diagnostic already
|
|
664
|
-
const dupDiags = this.grammar.takeDiagnostics();
|
|
665
|
-
for (const d of dupDiags) {
|
|
666
|
-
this.addDiagnostic(d);
|
|
667
|
-
}
|
|
668
|
-
frame.objectExpectation = "colon";
|
|
669
|
-
this.isSkippingValue = true;
|
|
670
|
-
this.skipValueDepth = 0;
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
frame.objectExpectation = "colon";
|
|
674
|
-
}
|
|
675
|
-
else if (frame.objectExpectation === "value") {
|
|
676
|
-
// This is a string value for an object field
|
|
677
|
-
const path = this.grammar.currentValuePath();
|
|
678
|
-
this.events.emitValueCommitted(path, token.value, [
|
|
679
|
-
token.byteStart,
|
|
680
|
-
token.byteEnd,
|
|
681
|
-
]);
|
|
682
|
-
this.snapshot_.processEvent({
|
|
683
|
-
type: "value_committed",
|
|
684
|
-
sequence: 0,
|
|
685
|
-
path,
|
|
686
|
-
operation: "add",
|
|
687
|
-
value: token.value,
|
|
688
|
-
byteRange: [token.byteStart, token.byteEnd],
|
|
689
|
-
});
|
|
690
|
-
frame.objectExpectation = "comma_or_end";
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
else if (frame.containerType === "array") {
|
|
694
|
-
if (frame.arrayExpectation !== "first_value_or_end" &&
|
|
695
|
-
frame.arrayExpectation !== "value_after_comma") {
|
|
696
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected string in array", false));
|
|
697
|
-
this.terminal = true;
|
|
698
|
-
this.syntax_ = "invalid";
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
// Array element
|
|
702
|
-
const path = this.grammar.currentValuePath();
|
|
703
|
-
this.events.emitValueCommitted(path, token.value, [
|
|
704
|
-
token.byteStart,
|
|
705
|
-
token.byteEnd,
|
|
706
|
-
]);
|
|
707
|
-
this.snapshot_.processEvent({
|
|
708
|
-
type: "value_committed",
|
|
709
|
-
sequence: 0,
|
|
710
|
-
path,
|
|
711
|
-
operation: "add",
|
|
712
|
-
value: token.value,
|
|
713
|
-
byteRange: [token.byteStart, token.byteEnd],
|
|
714
|
-
});
|
|
715
|
-
this.grammar.advanceArrayIndex();
|
|
716
|
-
frame.arrayExpectation = "comma_or_end";
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
handleNumber(token) {
|
|
720
|
-
const value = Number(token.value);
|
|
721
|
-
this.commitScalar(value, token);
|
|
722
|
-
}
|
|
723
|
-
handleLiteral(token) {
|
|
724
|
-
let value;
|
|
725
|
-
if (token.type === TokenType.True)
|
|
726
|
-
value = true;
|
|
727
|
-
else if (token.type === TokenType.False)
|
|
728
|
-
value = false;
|
|
729
|
-
else
|
|
730
|
-
value = null;
|
|
731
|
-
this.commitScalar(value, token);
|
|
732
|
-
}
|
|
733
|
-
commitScalar(value, token) {
|
|
734
|
-
const frame = this.grammar.current;
|
|
735
|
-
if (!frame) {
|
|
736
|
-
// Root-level scalar
|
|
737
|
-
this.hasSeenValue = true;
|
|
738
|
-
this.rootComplete = true;
|
|
739
|
-
this.syntax_ = "root_complete";
|
|
740
|
-
this.events.emitValueCommitted("", value, [
|
|
741
|
-
token.byteStart,
|
|
742
|
-
token.byteEnd,
|
|
743
|
-
]);
|
|
744
|
-
// Store root scalar in snapshot builder
|
|
745
|
-
// For root scalars, we directly set the root value
|
|
746
|
-
this.rootScalarValue = value;
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
if (frame.containerType === "object") {
|
|
750
|
-
if (frame.objectExpectation === "value") {
|
|
751
|
-
const path = this.grammar.currentValuePath();
|
|
752
|
-
this.events.emitValueCommitted(path, value, [
|
|
753
|
-
token.byteStart,
|
|
754
|
-
token.byteEnd,
|
|
755
|
-
]);
|
|
756
|
-
this.snapshot_.processEvent({
|
|
757
|
-
type: "value_committed",
|
|
758
|
-
sequence: 0,
|
|
759
|
-
path,
|
|
760
|
-
operation: "add",
|
|
761
|
-
value,
|
|
762
|
-
byteRange: [token.byteStart, token.byteEnd],
|
|
763
|
-
});
|
|
764
|
-
frame.objectExpectation = "comma_or_end";
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
else if (frame.containerType === "array") {
|
|
768
|
-
if (frame.arrayExpectation !== "first_value_or_end" &&
|
|
769
|
-
frame.arrayExpectation !== "value_after_comma") {
|
|
770
|
-
this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected scalar in array", false));
|
|
771
|
-
this.terminal = true;
|
|
772
|
-
this.syntax_ = "invalid";
|
|
773
|
-
return;
|
|
774
|
-
}
|
|
775
|
-
const path = this.grammar.currentValuePath();
|
|
776
|
-
this.events.emitValueCommitted(path, value, [
|
|
777
|
-
token.byteStart,
|
|
778
|
-
token.byteEnd,
|
|
779
|
-
]);
|
|
780
|
-
this.snapshot_.processEvent({
|
|
781
|
-
type: "value_committed",
|
|
782
|
-
sequence: 0,
|
|
783
|
-
path,
|
|
784
|
-
operation: "add",
|
|
785
|
-
value,
|
|
786
|
-
byteRange: [token.byteStart, token.byteEnd],
|
|
787
|
-
});
|
|
788
|
-
this.grammar.advanceArrayIndex();
|
|
789
|
-
frame.arrayExpectation = "comma_or_end";
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
commitContainerToParent(_containerPath, _token) {
|
|
793
|
-
const parentFrame = this.grammar.current;
|
|
794
|
-
if (!parentFrame)
|
|
795
|
-
return;
|
|
796
|
-
if (parentFrame.containerType === "object") {
|
|
797
|
-
// The closed container was a value in the parent object
|
|
798
|
-
// The value_committed for the object field is implicitly done
|
|
799
|
-
// when the container closes
|
|
800
|
-
parentFrame.objectExpectation = "comma_or_end";
|
|
801
|
-
}
|
|
802
|
-
else if (parentFrame.containerType === "array") {
|
|
803
|
-
this.grammar.advanceArrayIndex();
|
|
804
|
-
parentFrame.arrayExpectation = "comma_or_end";
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// -----------------------------------------------------------------------
|
|
808
|
-
// Helpers
|
|
809
|
-
// -----------------------------------------------------------------------
|
|
810
|
-
addDiagnostic(diag) {
|
|
811
|
-
// Freeze before storing: snapshot()/finish() freeze the ARRAY they
|
|
812
|
-
// return (Object.freeze([...this.allDiagnostics])), but that's shallow
|
|
813
|
-
// - it doesn't protect the Diagnostic objects themselves, which are the
|
|
814
|
-
// *same* references stored here. Without this, a consumer mutating a
|
|
815
|
-
// field on a diagnostic object obtained from one snapshot()/finish()
|
|
816
|
-
// call corrupts this parser's internal history, silently visible in
|
|
817
|
-
// every later snapshot()/finish() call.
|
|
818
|
-
Object.freeze(diag);
|
|
819
|
-
// Cap accumulation independently of the EventBuilder's own queue limit:
|
|
820
|
-
// a consumer who drains events after every push() keeps that queue
|
|
821
|
-
// short, which would otherwise let this array grow without bound for
|
|
822
|
-
// the lifetime of the parser (see limits.maxQueuedEvents).
|
|
823
|
-
if (diag.severity === "fatal")
|
|
824
|
-
this.everHadFatalDiagnostic = true;
|
|
825
|
-
if ((diag.severity === "error" || diag.severity === "fatal") &&
|
|
826
|
-
!diag.recoverable) {
|
|
827
|
-
this.everHadNonRecoverableError = true;
|
|
828
|
-
}
|
|
829
|
-
if (this.allDiagnostics.length < this.limits.maxQueuedEvents) {
|
|
830
|
-
this.allDiagnostics.push(diag);
|
|
831
|
-
}
|
|
832
|
-
this.events.emitDiagnostic(diag);
|
|
833
|
-
}
|
|
834
|
-
addRepair(repair) {
|
|
835
|
-
// See addDiagnostic()'s matching comment: freeze before storing so
|
|
836
|
-
// external mutation of a repair object obtained from a prior
|
|
837
|
-
// snapshot()/finish() call can't corrupt this parser's internal
|
|
838
|
-
// history. byteRange is a nested array - Object.freeze on the repair
|
|
839
|
-
// itself doesn't reach into it, so it needs its own freeze.
|
|
840
|
-
Object.freeze(repair.byteRange);
|
|
841
|
-
Object.freeze(repair);
|
|
842
|
-
if (repair.impact === "structural" || repair.impact === "lossy") {
|
|
843
|
-
this.everHadStructuralOrLossyRepair = true;
|
|
844
|
-
}
|
|
845
|
-
if (repair.code === "R_CLOSE_CONTAINER") {
|
|
846
|
-
this.everHadCloseContainerRepair = true;
|
|
847
|
-
}
|
|
848
|
-
if (this.allRepairs.length < this.limits.maxQueuedEvents) {
|
|
849
|
-
this.allRepairs.push(repair);
|
|
850
|
-
}
|
|
851
|
-
this.events.emitRepairApplied(repair);
|
|
852
|
-
}
|
|
853
|
-
isExecutable() {
|
|
854
|
-
if (this.phase !== "finished")
|
|
855
|
-
return false;
|
|
856
|
-
if (!this.rootComplete)
|
|
857
|
-
return false;
|
|
858
|
-
// this.terminal/syntax_ is the same authoritative "something went
|
|
859
|
-
// wrong" signal determineOutcome() checks first (syntax_ === "invalid").
|
|
860
|
-
// Some fatal conditions — e.g. EventBuilder's own event-queue-capacity
|
|
861
|
-
// cutoff (see semantic/builder.ts's enqueue()) — set these directly
|
|
862
|
-
// without going through addDiagnostic(), so they don't set
|
|
863
|
-
// everHadFatalDiagnostic below. Checking them explicitly here closes
|
|
864
|
-
// that gap instead of only catching diagnostics that happen to have
|
|
865
|
-
// been routed through addDiagnostic().
|
|
866
|
-
if (this.terminal || this.syntax_ === "invalid")
|
|
867
|
-
return false;
|
|
868
|
-
const reason = this.finishMeta?.reason ?? "unknown";
|
|
869
|
-
if (reason === "length" ||
|
|
870
|
-
reason === "network_error" ||
|
|
871
|
-
reason === "provider_error" ||
|
|
872
|
-
reason === "cancelled" ||
|
|
873
|
-
reason === "unknown") {
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
if (this.grammar.hasDuplicate)
|
|
877
|
-
return false;
|
|
878
|
-
if (this.hasFatalDiagnostic())
|
|
879
|
-
return false;
|
|
880
|
-
if (this.hasNonRecoverableError())
|
|
881
|
-
return false;
|
|
882
|
-
if (this.everHadStructuralOrLossyRepair)
|
|
883
|
-
return false;
|
|
884
|
-
const pendingInfo = this.scanner.getPendingInfo();
|
|
885
|
-
if (pendingInfo.type !== "none")
|
|
886
|
-
return false;
|
|
887
|
-
if (this.trailingDataSeen)
|
|
888
|
-
return false;
|
|
889
|
-
return true;
|
|
890
|
-
}
|
|
891
|
-
hasFatalDiagnostic() {
|
|
892
|
-
return this.everHadFatalDiagnostic;
|
|
893
|
-
}
|
|
894
|
-
hasNonRecoverableError() {
|
|
895
|
-
return this.everHadNonRecoverableError;
|
|
896
|
-
}
|
|
897
|
-
determineOutcome(reason) {
|
|
898
|
-
if (this.syntax_ === "invalid")
|
|
899
|
-
return "invalid";
|
|
900
|
-
if (this.hasFatalDiagnostic())
|
|
901
|
-
return "invalid";
|
|
902
|
-
if (this.grammar.hasDuplicate)
|
|
903
|
-
return "invalid";
|
|
904
|
-
// Check if salvaged
|
|
905
|
-
if (this.everHadCloseContainerRepair)
|
|
906
|
-
return "salvaged";
|
|
907
|
-
if (!this.rootComplete)
|
|
908
|
-
return "truncated";
|
|
909
|
-
if (reason !== "complete" && reason !== "unknown") {
|
|
910
|
-
// Reason indicates stream was cut off, but root is magically complete?
|
|
911
|
-
// Since it's complete, the parsed JSON is fully intact.
|
|
912
|
-
// We consider it valid, but potentially non-executable depending on policy.
|
|
913
|
-
return "valid";
|
|
914
|
-
}
|
|
915
|
-
return "valid";
|
|
916
|
-
}
|
|
917
|
-
buildStableValue() {
|
|
918
|
-
if (this.rootScalarValue !== undefined)
|
|
919
|
-
return this.rootScalarValue;
|
|
920
|
-
return this.snapshot_.getStableValue();
|
|
921
|
-
}
|
|
922
|
-
buildPendingTokens() {
|
|
923
|
-
const pending = [];
|
|
924
|
-
const info = this.scanner.getPendingInfo();
|
|
925
|
-
if (info.type !== "none") {
|
|
926
|
-
pending.push({
|
|
927
|
-
type: info.type === "object_key" ? "object_key" : info.type,
|
|
928
|
-
path: this.grammar.currentValuePath(),
|
|
929
|
-
buffered: info.buffer,
|
|
930
|
-
byteOffset: info.byteStart,
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
return pending;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Get the UTF-8 byte length of a character.
|
|
938
|
-
*/
|
|
939
|
-
function getUtf8ByteLength(ch) {
|
|
940
|
-
const code = ch.codePointAt(0) ?? 0;
|
|
941
|
-
if (code <= 0x7f)
|
|
942
|
-
return 1;
|
|
943
|
-
if (code <= 0x7ff)
|
|
944
|
-
return 2;
|
|
945
|
-
if (code <= 0xffff)
|
|
946
|
-
return 3;
|
|
947
|
-
return 4;
|
|
948
|
-
}
|
|
949
|
-
//# sourceMappingURL=parser.js.map
|