@openrewrite/rewrite 8.69.0-20251211-160325 → 8.69.0-20251212-112414

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.
Files changed (80) hide show
  1. package/dist/cli/cli-utils.d.ts.map +1 -1
  2. package/dist/cli/cli-utils.js +110 -71
  3. package/dist/cli/cli-utils.js.map +1 -1
  4. package/dist/javascript/package-json-parser.d.ts +0 -5
  5. package/dist/javascript/package-json-parser.d.ts.map +1 -1
  6. package/dist/javascript/package-json-parser.js +2 -12
  7. package/dist/javascript/package-json-parser.js.map +1 -1
  8. package/dist/javascript/package-manager.d.ts +72 -1
  9. package/dist/javascript/package-manager.d.ts.map +1 -1
  10. package/dist/javascript/package-manager.js +173 -2
  11. package/dist/javascript/package-manager.js.map +1 -1
  12. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -1
  13. package/dist/javascript/recipes/add-dependency.js +11 -8
  14. package/dist/javascript/recipes/add-dependency.js.map +1 -1
  15. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  16. package/dist/javascript/recipes/upgrade-dependency-version.js +11 -8
  17. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  18. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  19. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +11 -8
  20. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  21. package/dist/json/parser.d.ts.map +1 -1
  22. package/dist/json/parser.js +355 -123
  23. package/dist/json/parser.js.map +1 -1
  24. package/dist/json/tree.d.ts +5 -1
  25. package/dist/json/tree.d.ts.map +1 -1
  26. package/dist/json/tree.js +157 -7
  27. package/dist/json/tree.js.map +1 -1
  28. package/dist/print.d.ts.map +1 -1
  29. package/dist/print.js +5 -1
  30. package/dist/print.js.map +1 -1
  31. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  32. package/dist/rpc/request/get-languages.js +1 -0
  33. package/dist/rpc/request/get-languages.js.map +1 -1
  34. package/dist/rpc/server.d.ts +1 -0
  35. package/dist/rpc/server.d.ts.map +1 -1
  36. package/dist/rpc/server.js +1 -0
  37. package/dist/rpc/server.js.map +1 -1
  38. package/dist/version.txt +1 -1
  39. package/dist/yaml/index.d.ts +6 -0
  40. package/dist/yaml/index.d.ts.map +1 -0
  41. package/dist/yaml/index.js +37 -0
  42. package/dist/yaml/index.js.map +1 -0
  43. package/dist/yaml/parser.d.ts +6 -0
  44. package/dist/yaml/parser.d.ts.map +1 -0
  45. package/dist/yaml/parser.js +803 -0
  46. package/dist/yaml/parser.js.map +1 -0
  47. package/dist/yaml/print.d.ts +2 -0
  48. package/dist/yaml/print.d.ts.map +1 -0
  49. package/dist/yaml/print.js +234 -0
  50. package/dist/yaml/print.js.map +1 -0
  51. package/dist/yaml/rpc.d.ts +2 -0
  52. package/dist/yaml/rpc.d.ts.map +1 -0
  53. package/dist/yaml/rpc.js +264 -0
  54. package/dist/yaml/rpc.js.map +1 -0
  55. package/dist/yaml/tree.d.ts +188 -0
  56. package/dist/yaml/tree.d.ts.map +1 -0
  57. package/dist/yaml/tree.js +117 -0
  58. package/dist/yaml/tree.js.map +1 -0
  59. package/dist/yaml/visitor.d.ts +19 -0
  60. package/dist/yaml/visitor.d.ts.map +1 -0
  61. package/dist/yaml/visitor.js +170 -0
  62. package/dist/yaml/visitor.js.map +1 -0
  63. package/package.json +6 -1
  64. package/src/cli/cli-utils.ts +112 -35
  65. package/src/javascript/package-json-parser.ts +2 -12
  66. package/src/javascript/package-manager.ts +179 -3
  67. package/src/javascript/recipes/add-dependency.ts +16 -10
  68. package/src/javascript/recipes/upgrade-dependency-version.ts +16 -10
  69. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +15 -9
  70. package/src/json/parser.ts +443 -146
  71. package/src/json/tree.ts +159 -7
  72. package/src/print.ts +6 -2
  73. package/src/rpc/request/get-languages.ts +1 -0
  74. package/src/rpc/server.ts +1 -0
  75. package/src/yaml/index.ts +21 -0
  76. package/src/yaml/parser.ts +850 -0
  77. package/src/yaml/print.ts +212 -0
  78. package/src/yaml/rpc.ts +248 -0
  79. package/src/yaml/tree.ts +281 -0
  80. package/src/yaml/visitor.ts +146 -0
@@ -14,11 +14,55 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import {emptyMarkers, markers, MarkersKind, ParseExceptionResult} from "../markers";
17
- import {Parser, ParserInput, parserInputRead, ParserSourceReader} from "../parser";
17
+ import {Parser, ParserInput, parserInputRead} from "../parser";
18
18
  import {randomId} from "../uuid";
19
19
  import {SourceFile} from "../tree";
20
20
  import {emptySpace, Json, space} from "./tree";
21
21
  import {ParseError, ParseErrorKind} from "../parse-error";
22
+ import {createScanner} from "jsonc-parser";
23
+
24
+ // Define token types locally to avoid const enum issues with isolatedModules
25
+ const Token = {
26
+ OpenBraceToken: 1,
27
+ CloseBraceToken: 2,
28
+ OpenBracketToken: 3,
29
+ CloseBracketToken: 4,
30
+ CommaToken: 5,
31
+ ColonToken: 6,
32
+ NullKeyword: 7,
33
+ TrueKeyword: 8,
34
+ FalseKeyword: 9,
35
+ StringLiteral: 10,
36
+ NumericLiteral: 11,
37
+ LineCommentTrivia: 12,
38
+ BlockCommentTrivia: 13,
39
+ LineBreakTrivia: 14,
40
+ Trivia: 15,
41
+ Unknown: 16,
42
+ EOF: 17
43
+ } as const;
44
+
45
+ type TokenType = typeof Token[keyof typeof Token];
46
+
47
+ const TokenNames: Record<number, string> = {
48
+ [Token.OpenBraceToken]: 'OpenBraceToken',
49
+ [Token.CloseBraceToken]: 'CloseBraceToken',
50
+ [Token.OpenBracketToken]: 'OpenBracketToken',
51
+ [Token.CloseBracketToken]: 'CloseBracketToken',
52
+ [Token.CommaToken]: 'CommaToken',
53
+ [Token.ColonToken]: 'ColonToken',
54
+ [Token.NullKeyword]: 'NullKeyword',
55
+ [Token.TrueKeyword]: 'TrueKeyword',
56
+ [Token.FalseKeyword]: 'FalseKeyword',
57
+ [Token.StringLiteral]: 'StringLiteral',
58
+ [Token.NumericLiteral]: 'NumericLiteral',
59
+ [Token.LineCommentTrivia]: 'LineCommentTrivia',
60
+ [Token.BlockCommentTrivia]: 'BlockCommentTrivia',
61
+ [Token.LineBreakTrivia]: 'LineBreakTrivia',
62
+ [Token.Trivia]: 'Trivia',
63
+ [Token.Unknown]: 'Unknown',
64
+ [Token.EOF]: 'EOF'
65
+ };
22
66
 
23
67
  export class JsonParser extends Parser {
24
68
 
@@ -26,11 +70,11 @@ export class JsonParser extends Parser {
26
70
  for (const sourcePath of sourcePaths) {
27
71
  try {
28
72
  yield {
29
- ...new ParseJsonReader(sourcePath).parse(),
73
+ ...new JsoncParserReader(parserInputRead(sourcePath)).parse(),
30
74
  sourcePath: this.relativePath(sourcePath)
31
75
  };
32
76
  } catch (e: any) {
33
- // Return a ParseError for files that can't be parsed (e.g., JSONC with comments)
77
+ // Return a ParseError for files that can't be parsed
34
78
  const text = parserInputRead(sourcePath);
35
79
  const parseError: ParseError = {
36
80
  kind: ParseErrorKind,
@@ -51,177 +95,393 @@ export class JsonParser extends Parser {
51
95
  }
52
96
  }
53
97
 
54
- class ParseJsonReader extends ParserSourceReader {
55
- constructor(sourcePath: ParserInput) {
56
- super(sourcePath);
98
+ /**
99
+ * Parser that uses jsonc-parser's scanner for tokenization.
100
+ * This is significantly faster than our custom character-by-character parsing.
101
+ */
102
+ class JsoncParserReader {
103
+ private readonly source: string;
104
+ private readonly scanner: ReturnType<typeof createScanner>;
105
+ // Use explicit number type to prevent TypeScript from narrowing after switch cases
106
+ // (TypeScript doesn't know that consumeTrivia() and advance() modify this.token)
107
+ private token: number = 0;
108
+ private tokenOffset: number = 0;
109
+ private tokenLength: number = 0;
110
+ private tokenValue: string = '';
111
+
112
+ constructor(source: string) {
113
+ this.source = source;
114
+ // ignoreTrivia = false to get whitespace and comments
115
+ this.scanner = createScanner(source, false);
116
+ this.advance();
117
+ }
118
+
119
+ private advance(): number {
120
+ this.token = this.scanner.scan();
121
+ this.tokenOffset = this.scanner.getTokenOffset();
122
+ this.tokenLength = this.scanner.getTokenLength();
123
+ this.tokenValue = this.scanner.getTokenValue();
124
+ return this.token;
125
+ }
126
+
127
+ /**
128
+ * Get current token. This method helps TypeScript understand that the token
129
+ * may have changed after calling consumeTrivia() or advance().
130
+ */
131
+ private currentToken(): number {
132
+ return this.token;
133
+ }
134
+
135
+ /**
136
+ * Consumes all trivia (whitespace and comments) and returns them as a single string.
137
+ */
138
+ private consumeTrivia(): string {
139
+ let trivia = '';
140
+ while (
141
+ this.token === Token.Trivia ||
142
+ this.token === Token.LineBreakTrivia ||
143
+ this.token === Token.LineCommentTrivia ||
144
+ this.token === Token.BlockCommentTrivia
145
+ ) {
146
+ trivia += this.source.slice(this.tokenOffset, this.tokenOffset + this.tokenLength);
147
+ this.advance();
148
+ }
149
+ return trivia;
57
150
  }
58
151
 
59
- private prefix() {
60
- return space(this.whitespace());
152
+ private prefix(): Json.Space {
153
+ return space(this.consumeTrivia());
61
154
  }
62
155
 
63
156
  parse(): Omit<Json.Document, "sourcePath"> {
157
+ const prefix = this.prefix();
158
+
159
+ // Handle empty document
160
+ if (this.token === Token.EOF) {
161
+ return {
162
+ kind: Json.Kind.Document,
163
+ id: randomId(),
164
+ prefix,
165
+ markers: emptyMarkers,
166
+ value: {
167
+ kind: Json.Kind.Literal,
168
+ id: randomId(),
169
+ prefix: emptySpace,
170
+ markers: emptyMarkers,
171
+ source: this.source,
172
+ value: ''
173
+ } satisfies Json.Literal as Json.Literal as Json.Value,
174
+ eof: emptySpace
175
+ };
176
+ }
177
+
178
+ const value = this.parseValue() as Json.Value;
179
+ const eof = this.prefix();
180
+
64
181
  return {
65
182
  kind: Json.Kind.Document,
66
183
  id: randomId(),
67
- prefix: this.prefix(),
184
+ prefix,
68
185
  markers: emptyMarkers,
69
- value: this.json(JSON.parse(this.source)) as Json.Value,
70
- eof: space(this.source.slice(this.cursor))
71
- }
186
+ value,
187
+ eof
188
+ };
72
189
  }
73
190
 
74
- private json(parsed: any): Json {
191
+ private parseValue(): Json {
192
+ const prefix = this.prefix();
75
193
  const base = {
76
194
  id: randomId(),
77
- prefix: this.prefix(),
195
+ prefix,
78
196
  markers: emptyMarkers
197
+ };
198
+
199
+ switch (this.token) {
200
+ case Token.OpenBraceToken:
201
+ return this.parseObject(base);
202
+ case Token.OpenBracketToken:
203
+ return this.parseArray(base);
204
+ case Token.StringLiteral:
205
+ return this.parseStringLiteral(base);
206
+ case Token.NumericLiteral:
207
+ return this.parseNumericLiteral(base);
208
+ case Token.TrueKeyword:
209
+ this.advance();
210
+ return {
211
+ kind: Json.Kind.Literal,
212
+ ...base,
213
+ source: 'true',
214
+ value: true
215
+ } satisfies Json.Literal as Json.Literal;
216
+ case Token.FalseKeyword:
217
+ this.advance();
218
+ return {
219
+ kind: Json.Kind.Literal,
220
+ ...base,
221
+ source: 'false',
222
+ value: false
223
+ } satisfies Json.Literal as Json.Literal;
224
+ case Token.NullKeyword:
225
+ this.advance();
226
+ return {
227
+ kind: Json.Kind.Literal,
228
+ ...base,
229
+ source: 'null',
230
+ value: null
231
+ } satisfies Json.Literal as Json.Literal;
232
+ default:
233
+ throw new Error(`Unexpected token ${TokenNames[this.token] || this.token} at offset ${this.tokenOffset}`);
79
234
  }
80
- if (Array.isArray(parsed)) {
81
- this.cursor++; // skip '['
82
- let values: Json.RightPadded<Json.Value>[];
83
- if (parsed.length === 0) {
84
- // Empty array - capture whitespace in an Empty element
85
- const afterWhitespace = this.whitespace();
86
- values = [{
87
- kind: Json.Kind.RightPadded,
88
- element: {
89
- kind: Json.Kind.Empty,
90
- id: randomId(),
91
- prefix: emptySpace,
92
- markers: emptyMarkers
93
- } satisfies Json.Empty as Json.Empty,
94
- after: space(afterWhitespace),
235
+ }
236
+
237
+ private parseObject(base: { id: string; prefix: Json.Space; markers: typeof emptyMarkers }): Json.Object {
238
+ this.advance(); // consume '{'
239
+
240
+ const members: Json.RightPadded<Json.Member>[] = [];
241
+
242
+ // Check for empty object
243
+ const afterOpen = this.consumeTrivia();
244
+ if (this.token === Token.CloseBraceToken) {
245
+ this.advance(); // consume '}'
246
+ members.push({
247
+ kind: Json.Kind.RightPadded,
248
+ element: {
249
+ kind: Json.Kind.Empty,
250
+ id: randomId(),
251
+ prefix: emptySpace,
95
252
  markers: emptyMarkers
96
- } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>];
97
- } else {
98
- values = parsed.map((p, i) => {
99
- const element = this.json(p) as Json.Value;
100
- const afterWhitespace = this.whitespace();
101
- // Check if there's a comma after this element
102
- const hasComma = this.source[this.cursor] === ',';
103
- if (hasComma) {
104
- this.cursor++; // skip ','
253
+ } satisfies Json.Empty as Json.Empty as unknown as Json.Member,
254
+ after: space(afterOpen),
255
+ markers: emptyMarkers
256
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>);
257
+ } else {
258
+ // Parse members
259
+ // Put back the trivia by prepending it to the key's prefix
260
+ let pendingTrivia = afterOpen;
261
+
262
+ while (true) {
263
+ const member = this.parseMember(pendingTrivia);
264
+ pendingTrivia = '';
265
+
266
+ const afterMember = this.consumeTrivia();
267
+
268
+ if (this.token === Token.CommaToken) {
269
+ this.advance(); // consume ','
270
+
271
+ // Check for trailing comma
272
+ const afterComma = this.consumeTrivia();
273
+ if (this.currentToken() === Token.CloseBraceToken) {
274
+ // Trailing comma
275
+ members.push({
276
+ kind: Json.Kind.RightPadded,
277
+ element: member,
278
+ after: space(afterMember),
279
+ markers: emptyMarkers
280
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>);
281
+
282
+ this.advance(); // consume '}'
283
+ members.push({
284
+ kind: Json.Kind.RightPadded,
285
+ element: {
286
+ kind: Json.Kind.Empty,
287
+ id: randomId(),
288
+ prefix: emptySpace,
289
+ markers: emptyMarkers
290
+ } satisfies Json.Empty as Json.Empty as unknown as Json.Member,
291
+ after: space(afterComma),
292
+ markers: emptyMarkers
293
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>);
294
+ break;
295
+ } else {
296
+ // More members - save trivia for next member's prefix
297
+ members.push({
298
+ kind: Json.Kind.RightPadded,
299
+ element: member,
300
+ after: space(afterMember),
301
+ markers: emptyMarkers
302
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>);
303
+ pendingTrivia = afterComma;
105
304
  }
106
- return {
305
+ } else if (this.token === Token.CloseBraceToken) {
306
+ this.advance(); // consume '}'
307
+ members.push({
107
308
  kind: Json.Kind.RightPadded,
108
- element,
109
- after: space(afterWhitespace),
309
+ element: member,
310
+ after: space(afterMember),
110
311
  markers: emptyMarkers
111
- } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>;
112
- });
312
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>);
313
+ break;
314
+ } else {
315
+ throw new Error(`Expected ',' or '}' at offset ${this.tokenOffset}, found ${TokenNames[this.token] || this.token}`);
316
+ }
113
317
  }
114
- this.cursor++; // skip ']'
115
- return {
116
- kind: Json.Kind.Array,
117
- ...base,
118
- values
119
- } satisfies Json.Array as Json.Array;
120
- } else if (parsed !== null && typeof parsed === "object") {
121
- this.cursor++; // skip '{'
122
- const keys = Object.keys(parsed);
123
- let members: Json.RightPadded<Json.Member>[];
124
- if (keys.length === 0) {
125
- // Empty object - capture whitespace in an Empty element
126
- const afterWhitespace = this.whitespace();
127
- members = [{
128
- kind: Json.Kind.RightPadded,
129
- element: {
130
- kind: Json.Kind.Empty,
131
- id: randomId(),
132
- prefix: emptySpace,
133
- markers: emptyMarkers
134
- } satisfies Json.Empty as Json.Empty as unknown as Json.Member,
135
- after: space(afterWhitespace),
318
+ }
319
+
320
+ return {
321
+ kind: Json.Kind.Object,
322
+ ...base,
323
+ members
324
+ } satisfies Json.Object as Json.Object;
325
+ }
326
+
327
+ private parseArray(base: { id: string; prefix: Json.Space; markers: typeof emptyMarkers }): Json.Array {
328
+ this.advance(); // consume '['
329
+
330
+ const values: Json.RightPadded<Json.Value>[] = [];
331
+
332
+ // Check for empty array
333
+ const afterOpen = this.consumeTrivia();
334
+ if (this.token === Token.CloseBracketToken) {
335
+ this.advance(); // consume ']'
336
+ values.push({
337
+ kind: Json.Kind.RightPadded,
338
+ element: {
339
+ kind: Json.Kind.Empty,
340
+ id: randomId(),
341
+ prefix: emptySpace,
342
+ markers: emptyMarkers
343
+ } satisfies Json.Empty as Json.Empty,
344
+ after: space(afterOpen),
345
+ markers: emptyMarkers
346
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>);
347
+ } else {
348
+ // Parse values - need to handle the trivia we already consumed
349
+ // by putting it back as a "pending" prefix
350
+ let pendingTrivia = afterOpen;
351
+
352
+ while (true) {
353
+ // For first value, prepend the afterOpen trivia
354
+ const valuePrefix = pendingTrivia + this.consumeTrivia();
355
+ pendingTrivia = '';
356
+
357
+ const valueBase = {
358
+ id: randomId(),
359
+ prefix: space(valuePrefix),
136
360
  markers: emptyMarkers
137
- } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>];
138
- } else {
139
- members = keys.map((key, i) => {
140
- const element = this.member(parsed, key);
141
- const afterWhitespace = this.whitespace();
142
- // Check if there's a comma after this element
143
- const hasComma = this.source[this.cursor] === ',';
144
- if (hasComma) {
145
- this.cursor++; // skip ','
361
+ };
362
+
363
+ let value: Json.Value;
364
+ switch (this.token) {
365
+ case Token.OpenBraceToken:
366
+ value = this.parseObject(valueBase);
367
+ break;
368
+ case Token.OpenBracketToken:
369
+ value = this.parseArray(valueBase);
370
+ break;
371
+ case Token.StringLiteral:
372
+ value = this.parseStringLiteral(valueBase);
373
+ break;
374
+ case Token.NumericLiteral:
375
+ value = this.parseNumericLiteral(valueBase);
376
+ break;
377
+ case Token.TrueKeyword:
378
+ this.advance();
379
+ value = {
380
+ kind: Json.Kind.Literal,
381
+ ...valueBase,
382
+ source: 'true',
383
+ value: true
384
+ } satisfies Json.Literal as Json.Literal;
385
+ break;
386
+ case Token.FalseKeyword:
387
+ this.advance();
388
+ value = {
389
+ kind: Json.Kind.Literal,
390
+ ...valueBase,
391
+ source: 'false',
392
+ value: false
393
+ } satisfies Json.Literal as Json.Literal;
394
+ break;
395
+ case Token.NullKeyword:
396
+ this.advance();
397
+ value = {
398
+ kind: Json.Kind.Literal,
399
+ ...valueBase,
400
+ source: 'null',
401
+ value: null
402
+ } satisfies Json.Literal as Json.Literal;
403
+ break;
404
+ default:
405
+ throw new Error(`Unexpected token ${TokenNames[this.token] || this.token} in array at offset ${this.tokenOffset}`);
406
+ }
407
+
408
+ const afterValue = this.consumeTrivia();
409
+
410
+ if (this.currentToken() === Token.CommaToken) {
411
+ this.advance(); // consume ','
412
+
413
+ // Check for trailing comma
414
+ const afterComma = this.consumeTrivia();
415
+ if (this.currentToken() === Token.CloseBracketToken) {
416
+ // Trailing comma
417
+ values.push({
418
+ kind: Json.Kind.RightPadded,
419
+ element: value,
420
+ after: space(afterValue),
421
+ markers: emptyMarkers
422
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>);
423
+
424
+ this.advance(); // consume ']'
425
+ values.push({
426
+ kind: Json.Kind.RightPadded,
427
+ element: {
428
+ kind: Json.Kind.Empty,
429
+ id: randomId(),
430
+ prefix: emptySpace,
431
+ markers: emptyMarkers
432
+ } satisfies Json.Empty as Json.Empty,
433
+ after: space(afterComma),
434
+ markers: emptyMarkers
435
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>);
436
+ break;
437
+ } else {
438
+ // More values
439
+ values.push({
440
+ kind: Json.Kind.RightPadded,
441
+ element: value,
442
+ after: space(afterValue),
443
+ markers: emptyMarkers
444
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>);
445
+ pendingTrivia = afterComma;
146
446
  }
147
- return {
447
+ } else if (this.currentToken() === Token.CloseBracketToken) {
448
+ this.advance(); // consume ']'
449
+ values.push({
148
450
  kind: Json.Kind.RightPadded,
149
- element,
150
- after: space(afterWhitespace),
451
+ element: value,
452
+ after: space(afterValue),
151
453
  markers: emptyMarkers
152
- } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>;
153
- });
154
- }
155
- this.cursor++; // skip '}'
156
- return {
157
- kind: Json.Kind.Object,
158
- ...base,
159
- members
160
- } satisfies Json.Object as Json.Object;
161
- } else if (typeof parsed === "string") {
162
- // Extract original source to preserve escape sequences
163
- const sourceStart = this.cursor;
164
- this.cursor++; // skip opening quote
165
- while (this.cursor < this.source.length) {
166
- const char = this.source[this.cursor];
167
- if (char === '\\') {
168
- this.cursor += 2; // skip escape sequence
169
- } else if (char === '"') {
170
- this.cursor++; // skip closing quote
454
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>);
171
455
  break;
172
456
  } else {
173
- this.cursor++;
174
- }
175
- }
176
- const source = this.source.slice(sourceStart, this.cursor);
177
- return {
178
- kind: Json.Kind.Literal,
179
- ...base,
180
- source,
181
- value: parsed
182
- } satisfies Json.Literal as Json.Literal;
183
- } else if (typeof parsed === "number") {
184
- // Extract original source to preserve precision for large numbers
185
- const sourceStart = this.cursor;
186
- // Numbers can have optional sign, digits, decimal point, and exponent
187
- while (this.cursor < this.source.length) {
188
- const char = this.source[this.cursor];
189
- if (/[\d.eE+\-]/.test(char)) {
190
- this.cursor++;
191
- } else {
192
- break;
457
+ throw new Error(`Expected ',' or ']' at offset ${this.tokenOffset}, found ${TokenNames[this.currentToken()] || this.currentToken()}`);
193
458
  }
194
459
  }
195
- const source = this.source.slice(sourceStart, this.cursor);
196
- return {
197
- kind: Json.Kind.Literal,
198
- ...base,
199
- source,
200
- value: parsed,
201
- } satisfies Json.Literal as Json.Literal;
202
- } else if (typeof parsed === "boolean") {
203
- const source = parsed ? "true" : "false";
204
- this.cursor += source.length;
205
- return {
206
- kind: Json.Kind.Literal,
207
- ...base,
208
- source,
209
- value: parsed,
210
- } satisfies Json.Literal as Json.Literal;
211
- } else if (parsed === null) {
212
- this.cursor += 4; // "null".length
213
- return {
214
- kind: Json.Kind.Literal,
215
- ...base,
216
- source: "null",
217
- value: null,
218
- } satisfies Json.Literal as Json.Literal;
219
- } else {
220
- throw new Error(`Unsupported JSON type: ${typeof parsed}`);
221
460
  }
461
+
462
+ return {
463
+ kind: Json.Kind.Array,
464
+ ...base,
465
+ values
466
+ } satisfies Json.Array as Json.Array;
222
467
  }
223
468
 
224
- private member(parsed: any, key: string) {
469
+ private parseMember(pendingTrivia: string): Json.Member {
470
+ const keyPrefix = pendingTrivia + this.consumeTrivia();
471
+ const key = this.parseKey({
472
+ id: randomId(),
473
+ prefix: space(keyPrefix),
474
+ markers: emptyMarkers
475
+ });
476
+
477
+ const afterKey = this.consumeTrivia();
478
+ if (this.token !== Token.ColonToken) {
479
+ throw new Error(`Expected ':' at offset ${this.tokenOffset}, found ${TokenNames[this.token] || this.token}`);
480
+ }
481
+ this.advance(); // consume ':'
482
+
483
+ const value = this.parseValue() as Json.Value;
484
+
225
485
  return {
226
486
  kind: Json.Kind.Member,
227
487
  id: randomId(),
@@ -230,10 +490,47 @@ class ParseJsonReader extends ParserSourceReader {
230
490
  key: {
231
491
  kind: Json.Kind.RightPadded,
232
492
  markers: emptyMarkers,
233
- element: this.json(key) as Json.Key,
234
- after: space(this.sourceBefore(":")),
493
+ element: key as Json.Key,
494
+ after: space(afterKey)
235
495
  } satisfies Json.RightPadded<Json.Key> as Json.RightPadded<Json.Key>,
236
- value: this.json(parsed[key]) as Json.Value
496
+ value
237
497
  } satisfies Json.Member as Json.Member;
238
498
  }
499
+
500
+ /**
501
+ * Parses a key which is a string literal in standard JSON/JSONC.
502
+ * Note: jsonc-parser doesn't support JSON5 unquoted identifiers.
503
+ */
504
+ private parseKey(base: { id: string; prefix: Json.Space; markers: typeof emptyMarkers }): Json.Literal {
505
+ if (this.token !== Token.StringLiteral) {
506
+ throw new Error(`Expected string key at offset ${this.tokenOffset}, found ${TokenNames[this.token] || this.token}`);
507
+ }
508
+ return this.parseStringLiteral(base);
509
+ }
510
+
511
+ private parseStringLiteral(base: { id: string; prefix: Json.Space; markers: typeof emptyMarkers }): Json.Literal {
512
+ const source = this.source.slice(this.tokenOffset, this.tokenOffset + this.tokenLength);
513
+ const value = this.tokenValue;
514
+ this.advance();
515
+
516
+ return {
517
+ kind: Json.Kind.Literal,
518
+ ...base,
519
+ source,
520
+ value
521
+ } satisfies Json.Literal as Json.Literal;
522
+ }
523
+
524
+ private parseNumericLiteral(base: { id: string; prefix: Json.Space; markers: typeof emptyMarkers }): Json.Literal {
525
+ const source = this.source.slice(this.tokenOffset, this.tokenOffset + this.tokenLength);
526
+ const value = parseFloat(source);
527
+ this.advance();
528
+
529
+ return {
530
+ kind: Json.Kind.Literal,
531
+ ...base,
532
+ source,
533
+ value
534
+ } satisfies Json.Literal as Json.Literal;
535
+ }
239
536
  }