@openrewrite/rewrite 8.69.0-20251211-200238 → 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 (62) 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-manager.d.ts +72 -1
  5. package/dist/javascript/package-manager.d.ts.map +1 -1
  6. package/dist/javascript/package-manager.js +160 -1
  7. package/dist/javascript/package-manager.js.map +1 -1
  8. package/dist/javascript/recipes/add-dependency.d.ts.map +1 -1
  9. package/dist/javascript/recipes/add-dependency.js +11 -8
  10. package/dist/javascript/recipes/add-dependency.js.map +1 -1
  11. package/dist/javascript/recipes/upgrade-dependency-version.d.ts.map +1 -1
  12. package/dist/javascript/recipes/upgrade-dependency-version.js +11 -8
  13. package/dist/javascript/recipes/upgrade-dependency-version.js.map +1 -1
  14. package/dist/javascript/recipes/upgrade-transitive-dependency-version.d.ts.map +1 -1
  15. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js +11 -8
  16. package/dist/javascript/recipes/upgrade-transitive-dependency-version.js.map +1 -1
  17. package/dist/rpc/request/get-languages.d.ts.map +1 -1
  18. package/dist/rpc/request/get-languages.js +1 -0
  19. package/dist/rpc/request/get-languages.js.map +1 -1
  20. package/dist/rpc/server.d.ts +1 -0
  21. package/dist/rpc/server.d.ts.map +1 -1
  22. package/dist/rpc/server.js +1 -0
  23. package/dist/rpc/server.js.map +1 -1
  24. package/dist/version.txt +1 -1
  25. package/dist/yaml/index.d.ts +6 -0
  26. package/dist/yaml/index.d.ts.map +1 -0
  27. package/dist/yaml/index.js +37 -0
  28. package/dist/yaml/index.js.map +1 -0
  29. package/dist/yaml/parser.d.ts +6 -0
  30. package/dist/yaml/parser.d.ts.map +1 -0
  31. package/dist/yaml/parser.js +803 -0
  32. package/dist/yaml/parser.js.map +1 -0
  33. package/dist/yaml/print.d.ts +2 -0
  34. package/dist/yaml/print.d.ts.map +1 -0
  35. package/dist/yaml/print.js +234 -0
  36. package/dist/yaml/print.js.map +1 -0
  37. package/dist/yaml/rpc.d.ts +2 -0
  38. package/dist/yaml/rpc.d.ts.map +1 -0
  39. package/dist/yaml/rpc.js +264 -0
  40. package/dist/yaml/rpc.js.map +1 -0
  41. package/dist/yaml/tree.d.ts +188 -0
  42. package/dist/yaml/tree.d.ts.map +1 -0
  43. package/dist/yaml/tree.js +117 -0
  44. package/dist/yaml/tree.js.map +1 -0
  45. package/dist/yaml/visitor.d.ts +19 -0
  46. package/dist/yaml/visitor.d.ts.map +1 -0
  47. package/dist/yaml/visitor.js +170 -0
  48. package/dist/yaml/visitor.js.map +1 -0
  49. package/package.json +5 -1
  50. package/src/cli/cli-utils.ts +112 -35
  51. package/src/javascript/package-manager.ts +166 -2
  52. package/src/javascript/recipes/add-dependency.ts +16 -10
  53. package/src/javascript/recipes/upgrade-dependency-version.ts +16 -10
  54. package/src/javascript/recipes/upgrade-transitive-dependency-version.ts +15 -9
  55. package/src/rpc/request/get-languages.ts +1 -0
  56. package/src/rpc/server.ts +1 -0
  57. package/src/yaml/index.ts +21 -0
  58. package/src/yaml/parser.ts +850 -0
  59. package/src/yaml/print.ts +212 -0
  60. package/src/yaml/rpc.ts +248 -0
  61. package/src/yaml/tree.ts +281 -0
  62. package/src/yaml/visitor.ts +146 -0
@@ -0,0 +1,850 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {emptyMarkers, markers, MarkersKind, ParseExceptionResult} from "../markers";
17
+ import {Parser, ParserInput, parserInputRead} from "../parser";
18
+ import {randomId} from "../uuid";
19
+ import {SourceFile} from "../tree";
20
+ import {Yaml} from "./tree";
21
+ import {ParseError, ParseErrorKind} from "../parse-error";
22
+ import {Parser as YamlCstParser, CST} from "yaml";
23
+
24
+ // Types from yaml package CST
25
+ type CstToken = CST.Token;
26
+ type CstSourceToken = CST.SourceToken;
27
+ type CstDocument = CST.Document;
28
+ type CstDocumentEnd = CST.DocumentEnd;
29
+ type CstBlockMap = CST.BlockMap;
30
+ type CstBlockSequence = CST.BlockSequence;
31
+ type CstFlowCollection = CST.FlowCollection;
32
+ type CstFlowScalar = CST.FlowScalar;
33
+ type CstBlockScalar = CST.BlockScalar;
34
+ type CstCollectionItem = CST.CollectionItem;
35
+
36
+ export class YamlParser extends Parser {
37
+
38
+ async *parse(...sourcePaths: ParserInput[]): AsyncGenerator<SourceFile> {
39
+ for (const sourcePath of sourcePaths) {
40
+ const text = parserInputRead(sourcePath);
41
+ try {
42
+ yield {
43
+ ...new YamlCstReader(text).parse(),
44
+ sourcePath: this.relativePath(sourcePath)
45
+ };
46
+ } catch (e: any) {
47
+ // Return a ParseError for files that can't be parsed
48
+ const parseError: ParseError = {
49
+ kind: ParseErrorKind,
50
+ id: randomId(),
51
+ markers: markers({
52
+ kind: MarkersKind.ParseExceptionResult,
53
+ id: randomId(),
54
+ parserType: "YamlParser",
55
+ exceptionType: e.name || "Error",
56
+ message: e.message || "Unknown parse error"
57
+ } satisfies ParseExceptionResult as ParseExceptionResult),
58
+ sourcePath: this.relativePath(sourcePath),
59
+ text
60
+ };
61
+ yield parseError;
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Converts YAML CST from the 'yaml' package to our Yaml AST.
69
+ * The CST preserves all whitespace, comments, and formatting.
70
+ */
71
+ class YamlCstReader {
72
+ private readonly cstTokens: CstToken[];
73
+
74
+ constructor(source: string) {
75
+ const parser = new YamlCstParser();
76
+ this.cstTokens = [...parser.parse(source)];
77
+ }
78
+
79
+ parse(): Omit<Yaml.Documents, "sourcePath"> {
80
+ const documents: Yaml.Document[] = [];
81
+ let pendingPrefix = "";
82
+
83
+ for (let i = 0; i < this.cstTokens.length; i++) {
84
+ const token = this.cstTokens[i];
85
+
86
+ if (token.type === 'document') {
87
+ const cstDoc = token as CstDocument;
88
+ // Check if next token is doc-end
89
+ const nextToken = this.cstTokens[i + 1];
90
+ let docEnd: CstDocumentEnd | undefined;
91
+ if (nextToken?.type === 'doc-end') {
92
+ docEnd = nextToken as CstDocumentEnd;
93
+ i++; // Skip the doc-end in the main loop
94
+ }
95
+ const {doc, afterEnd} = this.convertDocument(cstDoc, docEnd, pendingPrefix);
96
+ documents.push(doc);
97
+ pendingPrefix = afterEnd; // Content after ... becomes next doc's prefix
98
+ } else if (token.type === 'doc-end') {
99
+ // Standalone doc-end without preceding document
100
+ const docEnd = token as CstDocumentEnd;
101
+ pendingPrefix += this.concatenateSources(docEnd.end || []);
102
+ } else if (token.type === 'comment' || token.type === 'newline' || token.type === 'space') {
103
+ // Content before document (comments, whitespace) becomes document prefix
104
+ pendingPrefix += (token as CstSourceToken).source;
105
+ }
106
+ }
107
+
108
+ return {
109
+ kind: Yaml.Kind.Documents,
110
+ id: randomId(),
111
+ prefix: "",
112
+ markers: emptyMarkers,
113
+ documents,
114
+ suffix: undefined
115
+ };
116
+ }
117
+
118
+ private convertDocument(cstDoc: CstDocument, cstDocEnd?: CstDocumentEnd, pendingPrefix: string = ""): {doc: Yaml.Document, afterEnd: string} {
119
+ // Extract prefix from document start tokens
120
+ // The document prefix is content BEFORE the --- marker
121
+ // Content AFTER --- goes into the block's first entry
122
+ const startTokens = cstDoc.start || [];
123
+ let prefix = pendingPrefix; // Content before this document
124
+ let explicit = false;
125
+ let afterDocStart = ""; // Content after ---
126
+ let seenDocStart = false;
127
+
128
+ for (const token of startTokens) {
129
+ if (token.type === 'doc-start') {
130
+ explicit = true;
131
+ seenDocStart = true;
132
+ } else if (seenDocStart) {
133
+ // Content after --- goes into block's prefix
134
+ afterDocStart += token.source;
135
+ } else {
136
+ // Content before --- goes into document prefix
137
+ prefix += token.source;
138
+ }
139
+ }
140
+
141
+ // Convert the document body and get trailing content
142
+ let block: Yaml.Block;
143
+ let trailing = "";
144
+ if (cstDoc.value) {
145
+ const result = this.convertTokenWithTrailing(cstDoc.value);
146
+ block = result.node as Yaml.Block;
147
+ trailing = result.trailing;
148
+
149
+ // Prepend the whitespace after --- to the block
150
+ if (afterDocStart) {
151
+ block = this.prependWhitespaceToValue(block, afterDocStart);
152
+ }
153
+ } else {
154
+ block = this.createEmptyScalar(afterDocStart);
155
+ }
156
+
157
+ // Convert document end, passing the trailing content from block
158
+ const {end, afterEnd} = this.convertDocumentEnd(cstDoc.end, cstDocEnd, trailing);
159
+
160
+ return {
161
+ doc: {
162
+ kind: Yaml.Kind.Document,
163
+ id: randomId(),
164
+ prefix,
165
+ markers: emptyMarkers,
166
+ explicit,
167
+ block,
168
+ end
169
+ },
170
+ afterEnd
171
+ };
172
+ }
173
+
174
+ private convertDocumentEnd(docEndTokens?: CstSourceToken[], cstDocEnd?: CstDocumentEnd, trailing: string = ""): {end: Yaml.DocumentEnd, afterEnd: string} {
175
+ // Prefix is the trailing content from the block, plus any content before ...
176
+ let prefix = trailing;
177
+ let explicit = false;
178
+ let afterEnd = "";
179
+
180
+ // Content after the document body (before ... if present)
181
+ if (docEndTokens) {
182
+ prefix += this.concatenateSources(docEndTokens);
183
+ }
184
+
185
+ // Explicit document end marker (...)
186
+ if (cstDocEnd) {
187
+ explicit = true;
188
+ // cstDocEnd.end contains tokens AFTER the ... marker - these become next doc's prefix
189
+ if (cstDocEnd.end) {
190
+ afterEnd = this.concatenateSources(cstDocEnd.end);
191
+ }
192
+ }
193
+
194
+ return {
195
+ end: {
196
+ kind: Yaml.Kind.DocumentEnd,
197
+ id: randomId(),
198
+ prefix,
199
+ markers: emptyMarkers,
200
+ explicit
201
+ },
202
+ afterEnd
203
+ };
204
+ }
205
+
206
+ private convertTokenWithTrailing(token: CstToken): {node: Yaml, trailing: string} {
207
+ switch (token.type) {
208
+ case 'block-map':
209
+ return this.convertBlockMapWithTrailing(token as CstBlockMap);
210
+ case 'block-seq':
211
+ return this.convertBlockSequenceWithTrailing(token as CstBlockSequence);
212
+ case 'flow-collection':
213
+ return this.convertFlowCollectionWithTrailing(token as CstFlowCollection);
214
+ case 'scalar':
215
+ case 'single-quoted-scalar':
216
+ case 'double-quoted-scalar':
217
+ return this.convertFlowScalarWithTrailing(token as CstFlowScalar);
218
+ case 'block-scalar':
219
+ return this.convertBlockScalarWithTrailing(token as CstBlockScalar);
220
+ case 'alias':
221
+ return this.convertAliasWithTrailing(token as CstFlowScalar);
222
+ default:
223
+ // For unknown types, create an empty scalar
224
+ return {node: this.createEmptyScalar(), trailing: ""};
225
+ }
226
+ }
227
+
228
+ private convertBlockMapWithTrailing(cst: CstBlockMap): {node: Yaml.Mapping, trailing: string} {
229
+ const entries: Yaml.MappingEntry[] = [];
230
+ let pendingPrefix = "";
231
+
232
+ for (const item of cst.items) {
233
+ if (item.key !== undefined || item.value !== undefined) {
234
+ const entry = this.convertMappingEntry(item, pendingPrefix);
235
+ entries.push(entry.entry);
236
+ pendingPrefix = entry.trailingContent;
237
+ } else {
238
+ // Entry with no key or value - capture start tokens (comments, whitespace)
239
+ // into pending prefix for the next entry or as trailing content
240
+ pendingPrefix += this.concatenateSources(item.start || []);
241
+ }
242
+ }
243
+
244
+ return {
245
+ node: {
246
+ kind: Yaml.Kind.Mapping,
247
+ id: randomId(),
248
+ prefix: "",
249
+ markers: emptyMarkers,
250
+ openingBracePrefix: undefined, // Block map has no braces
251
+ entries,
252
+ closingBracePrefix: undefined,
253
+ anchor: undefined,
254
+ tag: undefined
255
+ },
256
+ trailing: pendingPrefix // Trailing content from last entry
257
+ };
258
+ }
259
+
260
+ private convertMappingEntry(item: CstCollectionItem, pendingPrefix: string = ""): {entry: Yaml.MappingEntry, trailingContent: string} {
261
+ // Prefix comes from start tokens plus any pending prefix from previous entry's trailing content
262
+ const prefix = pendingPrefix + this.concatenateSources(item.start || []);
263
+
264
+ // Convert key and get its trailing content
265
+ let keyTrailing = "";
266
+ let key: Yaml.YamlKey;
267
+ if (item.key) {
268
+ const keyResult = this.convertTokenWithTrailing(item.key);
269
+ key = keyResult.node as Yaml.YamlKey;
270
+ keyTrailing = keyResult.trailing;
271
+ } else {
272
+ key = this.createEmptyScalar();
273
+ }
274
+
275
+ // Extract whitespace before ':' and any anchor/tag after it
276
+ let beforeMappingValueIndicator = keyTrailing;
277
+ let anchorForValue: Yaml.Anchor | undefined;
278
+ let tagForValue: Yaml.Tag | undefined;
279
+ let afterColonWhitespace = "";
280
+ let seenColon = false;
281
+
282
+ if (item.sep) {
283
+ const sepTokens = item.sep;
284
+ for (let i = 0; i < sepTokens.length; i++) {
285
+ const sepToken = sepTokens[i];
286
+ if (sepToken.type === 'map-value-ind') {
287
+ seenColon = true;
288
+ } else if (!seenColon) {
289
+ // Before the ':'
290
+ beforeMappingValueIndicator += sepToken.source;
291
+ } else if (sepToken.type === 'anchor') {
292
+ // Collect all remaining tokens after anchor as postfix
293
+ let postfix = "";
294
+ for (let j = i + 1; j < sepTokens.length; j++) {
295
+ const nextToken = sepTokens[j];
296
+ if (nextToken.type === 'tag') {
297
+ tagForValue = this.parseTagTokenWithSuffix(nextToken.source, postfix, sepTokens, j + 1);
298
+ postfix = "";
299
+ break; // Tag handler consumed remaining tokens
300
+ } else {
301
+ postfix += nextToken.source;
302
+ }
303
+ }
304
+ anchorForValue = {
305
+ kind: Yaml.Kind.Anchor,
306
+ id: randomId(),
307
+ prefix: afterColonWhitespace,
308
+ markers: emptyMarkers,
309
+ postfix,
310
+ key: sepToken.source.substring(1) // Remove &
311
+ };
312
+ afterColonWhitespace = "";
313
+ break; // We've consumed all remaining tokens
314
+ } else if (sepToken.type === 'tag') {
315
+ // Parse tag with its suffix (whitespace after the tag)
316
+ tagForValue = this.parseTagTokenWithSuffix(sepToken.source, afterColonWhitespace, sepTokens, i + 1);
317
+ afterColonWhitespace = ""; // Remaining whitespace is in tag suffix, not value prefix
318
+ break; // Tag handler consumed remaining tokens
319
+ } else {
320
+ // After the ':'
321
+ afterColonWhitespace += sepToken.source;
322
+ }
323
+ }
324
+ }
325
+
326
+ // Convert value
327
+ let value: Yaml.Block;
328
+ let valueTrailing = "";
329
+ if (item.value) {
330
+ const valueResult = this.convertTokenWithTrailing(item.value);
331
+ value = valueResult.node as Yaml.Block;
332
+ valueTrailing = valueResult.trailing;
333
+
334
+ // Apply anchor and tag if found in separator
335
+ if (anchorForValue && 'anchor' in value) {
336
+ value = {...value, anchor: anchorForValue} as Yaml.Block;
337
+ }
338
+ if (tagForValue && 'tag' in value) {
339
+ value = {...value, tag: tagForValue} as Yaml.Block;
340
+ }
341
+
342
+ // Prepend accumulated whitespace based on value type
343
+ if (afterColonWhitespace) {
344
+ value = this.prependWhitespaceToValue(value, afterColonWhitespace);
345
+ }
346
+ } else {
347
+ value = this.createEmptyScalar(afterColonWhitespace);
348
+ }
349
+
350
+ return {
351
+ entry: {
352
+ kind: Yaml.Kind.MappingEntry,
353
+ id: randomId(),
354
+ prefix,
355
+ markers: emptyMarkers,
356
+ key,
357
+ beforeMappingValueIndicator,
358
+ value
359
+ },
360
+ trailingContent: valueTrailing
361
+ };
362
+ }
363
+
364
+ private convertBlockSequenceWithTrailing(cst: CstBlockSequence): {node: Yaml.Sequence, trailing: string} {
365
+ const entries: Yaml.SequenceEntry[] = [];
366
+ let pendingPrefix = "";
367
+
368
+ for (const item of cst.items) {
369
+ const result = this.convertSequenceEntry(item, true, pendingPrefix);
370
+ entries.push(result.entry);
371
+ pendingPrefix = result.trailingContent;
372
+ }
373
+
374
+ return {
375
+ node: {
376
+ kind: Yaml.Kind.Sequence,
377
+ id: randomId(),
378
+ prefix: "",
379
+ markers: emptyMarkers,
380
+ openingBracketPrefix: undefined, // Block sequence has no brackets
381
+ entries,
382
+ closingBracketPrefix: undefined,
383
+ anchor: undefined,
384
+ tag: undefined
385
+ },
386
+ trailing: pendingPrefix // Trailing content from last entry
387
+ };
388
+ }
389
+
390
+ private convertSequenceEntry(item: CstCollectionItem, dash: boolean, pendingPrefix: string = ""): {entry: Yaml.SequenceEntry, trailingContent: string} {
391
+ // Build prefix from start tokens, but exclude the dash indicator itself
392
+ let prefix = pendingPrefix;
393
+ let afterDashSpace = "";
394
+ let seenDash = false;
395
+
396
+ for (const token of item.start || []) {
397
+ if (token.type === 'seq-item-ind') {
398
+ seenDash = true;
399
+ } else if (seenDash) {
400
+ afterDashSpace += token.source;
401
+ } else {
402
+ prefix += token.source;
403
+ }
404
+ }
405
+
406
+ // Convert value
407
+ let block: Yaml.Block;
408
+ let trailing = "";
409
+ if (item.value) {
410
+ const valueResult = this.convertTokenWithTrailing(item.value);
411
+ block = valueResult.node as Yaml.Block;
412
+ trailing = valueResult.trailing;
413
+ // Prepend the space after dash to the block's prefix
414
+ block = {...block, prefix: afterDashSpace + block.prefix} as Yaml.Block;
415
+ } else {
416
+ block = this.createEmptyScalar(afterDashSpace);
417
+ }
418
+
419
+ return {
420
+ entry: {
421
+ kind: Yaml.Kind.SequenceEntry,
422
+ id: randomId(),
423
+ prefix,
424
+ markers: emptyMarkers,
425
+ block,
426
+ dash,
427
+ trailingCommaPrefix: undefined // Block sequence has no commas
428
+ },
429
+ trailingContent: trailing
430
+ };
431
+ }
432
+
433
+ private convertFlowCollectionWithTrailing(cst: CstFlowCollection): {node: Yaml.Mapping | Yaml.Sequence, trailing: string} {
434
+ const isMap = cst.start.source === '{';
435
+ const openingPrefix = "";
436
+
437
+ // End tokens include closing bracket/brace plus any trailing content
438
+ // We need to separate the closing delimiter from trailing whitespace
439
+ const endTokens = cst.end || [];
440
+ let closingPrefix = "";
441
+ let trailing = "";
442
+ let seenClosing = false;
443
+
444
+ for (const token of endTokens) {
445
+ if (token.type === 'flow-map-end' || token.type === 'flow-seq-end') {
446
+ seenClosing = true;
447
+ } else if (seenClosing) {
448
+ trailing += token.source;
449
+ } else {
450
+ closingPrefix += token.source;
451
+ }
452
+ }
453
+
454
+ if (isMap) {
455
+ const entries: Yaml.MappingEntry[] = [];
456
+ let pendingPrefix = "";
457
+
458
+ for (const item of cst.items) {
459
+ if (item.key !== undefined || item.value !== undefined) {
460
+ const result = this.convertFlowMappingEntry(item, pendingPrefix);
461
+ entries.push(result.entry);
462
+ pendingPrefix = result.trailingContent;
463
+ } else {
464
+ // Empty item (trailing comma) - capture start tokens including the comma
465
+ pendingPrefix += this.concatenateSources(item.start || []);
466
+ }
467
+ }
468
+
469
+ // Trailing content from last entry becomes part of closing brace prefix
470
+ const finalClosingPrefix = pendingPrefix + closingPrefix;
471
+
472
+ return {
473
+ node: {
474
+ kind: Yaml.Kind.Mapping,
475
+ id: randomId(),
476
+ prefix: "",
477
+ markers: emptyMarkers,
478
+ openingBracePrefix: openingPrefix,
479
+ entries,
480
+ closingBracePrefix: finalClosingPrefix,
481
+ anchor: undefined,
482
+ tag: undefined
483
+ },
484
+ trailing
485
+ };
486
+ } else {
487
+ // Flow sequence [a, b, c]
488
+ const entries: Yaml.SequenceEntry[] = [];
489
+ let pendingPrefix = "";
490
+
491
+ for (let i = 0; i < cst.items.length; i++) {
492
+ const item = cst.items[i];
493
+ const isLast = i === cst.items.length - 1;
494
+ const result = this.convertFlowSequenceEntry(item, isLast, pendingPrefix);
495
+ entries.push(result.entry);
496
+ pendingPrefix = result.trailingContent;
497
+ }
498
+
499
+ // Trailing content from last entry becomes part of closing bracket prefix
500
+ const finalClosingPrefix = pendingPrefix + closingPrefix;
501
+
502
+ return {
503
+ node: {
504
+ kind: Yaml.Kind.Sequence,
505
+ id: randomId(),
506
+ prefix: "",
507
+ markers: emptyMarkers,
508
+ openingBracketPrefix: openingPrefix,
509
+ entries,
510
+ closingBracketPrefix: finalClosingPrefix,
511
+ anchor: undefined,
512
+ tag: undefined
513
+ },
514
+ trailing
515
+ };
516
+ }
517
+ }
518
+
519
+ private convertFlowMappingEntry(item: CstCollectionItem, pendingPrefix: string): {entry: Yaml.MappingEntry, trailingContent: string} {
520
+ const prefix = pendingPrefix + this.concatenateSources(item.start || []);
521
+
522
+ let keyTrailing = "";
523
+ let key: Yaml.YamlKey;
524
+ if (item.key) {
525
+ const keyResult = this.convertTokenWithTrailing(item.key);
526
+ key = keyResult.node as Yaml.YamlKey;
527
+ keyTrailing = keyResult.trailing;
528
+ } else {
529
+ key = this.createEmptyScalar();
530
+ }
531
+
532
+ let beforeMappingValueIndicator = keyTrailing;
533
+ let afterColonSpace = "";
534
+ let seenColon = false;
535
+
536
+ if (item.sep) {
537
+ for (const token of item.sep) {
538
+ if (token.type === 'map-value-ind') {
539
+ seenColon = true;
540
+ } else if (seenColon) {
541
+ afterColonSpace += token.source;
542
+ } else if (token.type === 'comma') {
543
+ // This is the comma after the entry, not before
544
+ // Skip it as we handle comma separately
545
+ } else {
546
+ beforeMappingValueIndicator += token.source;
547
+ }
548
+ }
549
+ }
550
+
551
+ let value: Yaml.Block;
552
+ let valueTrailing = "";
553
+ if (item.value) {
554
+ const valueResult = this.convertTokenWithTrailing(item.value);
555
+ value = valueResult.node as Yaml.Block;
556
+ valueTrailing = valueResult.trailing;
557
+ value = {...value, prefix: afterColonSpace + value.prefix} as Yaml.Block;
558
+ } else {
559
+ value = this.createEmptyScalar(afterColonSpace);
560
+ }
561
+
562
+ return {
563
+ entry: {
564
+ kind: Yaml.Kind.MappingEntry,
565
+ id: randomId(),
566
+ prefix,
567
+ markers: emptyMarkers,
568
+ key,
569
+ beforeMappingValueIndicator,
570
+ value
571
+ },
572
+ trailingContent: valueTrailing
573
+ };
574
+ }
575
+
576
+ private convertFlowSequenceEntry(item: CstCollectionItem, isLast: boolean, pendingPrefix: string): {entry: Yaml.SequenceEntry, trailingContent: string} {
577
+ // Start tokens may include comma from previous item
578
+ let prefix = pendingPrefix;
579
+ let hasComma = false;
580
+
581
+ for (const token of item.start || []) {
582
+ if (token.type === 'comma') {
583
+ hasComma = true;
584
+ } else {
585
+ prefix += token.source;
586
+ }
587
+ }
588
+
589
+ let block: Yaml.Block;
590
+ let trailing = "";
591
+
592
+ if (item.value) {
593
+ const valueResult = this.convertTokenWithTrailing(item.value);
594
+ block = valueResult.node as Yaml.Block;
595
+ trailing = valueResult.trailing;
596
+ } else if (item.key !== undefined) {
597
+ // Flow sequence can contain mapping entries: [a: 1, b: 2]
598
+ const entryResult = this.convertFlowMappingEntry(item, "");
599
+ block = {
600
+ kind: Yaml.Kind.Mapping,
601
+ id: randomId(),
602
+ prefix: "",
603
+ markers: emptyMarkers,
604
+ openingBracePrefix: undefined,
605
+ entries: [entryResult.entry],
606
+ closingBracePrefix: undefined,
607
+ anchor: undefined,
608
+ tag: undefined
609
+ };
610
+ trailing = entryResult.trailingContent;
611
+ } else {
612
+ block = this.createEmptyScalar();
613
+ }
614
+
615
+ // Trailing comma prefix captures space after value before comma
616
+ let trailingCommaPrefix: string | undefined;
617
+ if (!isLast) {
618
+ trailingCommaPrefix = trailing;
619
+ trailing = "";
620
+ }
621
+
622
+ return {
623
+ entry: {
624
+ kind: Yaml.Kind.SequenceEntry,
625
+ id: randomId(),
626
+ prefix,
627
+ markers: emptyMarkers,
628
+ block,
629
+ dash: false, // Flow sequences don't use dashes
630
+ trailingCommaPrefix
631
+ },
632
+ trailingContent: trailing
633
+ };
634
+ }
635
+
636
+ private convertFlowScalarWithTrailing(cst: CstFlowScalar): {node: Yaml.Scalar, trailing: string} {
637
+ const resolved = CST.resolveAsScalar(cst);
638
+ const value = resolved?.value ?? "";
639
+
640
+ // Determine scalar style
641
+ let style: Yaml.ScalarStyle;
642
+ switch (cst.type) {
643
+ case 'single-quoted-scalar':
644
+ style = Yaml.ScalarStyle.SINGLE_QUOTED;
645
+ break;
646
+ case 'double-quoted-scalar':
647
+ style = Yaml.ScalarStyle.DOUBLE_QUOTED;
648
+ break;
649
+ default:
650
+ style = Yaml.ScalarStyle.PLAIN;
651
+ }
652
+
653
+ // End tokens contain trailing whitespace/newlines
654
+ const trailing = this.concatenateSources(cst.end || []);
655
+
656
+ return {
657
+ node: {
658
+ kind: Yaml.Kind.Scalar,
659
+ id: randomId(),
660
+ prefix: "",
661
+ markers: emptyMarkers,
662
+ style,
663
+ anchor: undefined,
664
+ tag: undefined,
665
+ value
666
+ },
667
+ trailing
668
+ };
669
+ }
670
+
671
+ private convertBlockScalarWithTrailing(cst: CstBlockScalar): {node: Yaml.Scalar, trailing: string} {
672
+ // Use CST.stringify to get the exact original source including header
673
+ const fullSource = CST.stringify(cst);
674
+
675
+ // Determine style from the first character (| or >)
676
+ const headerChar = fullSource.charAt(0);
677
+ const style = headerChar === '|' ? Yaml.ScalarStyle.LITERAL : Yaml.ScalarStyle.FOLDED;
678
+
679
+ // The value is everything after the header indicator (| or >)
680
+ const value = fullSource.substring(1);
681
+
682
+ // Extract anchor and tag from props if present
683
+ let anchor: Yaml.Anchor | undefined;
684
+ let tag: Yaml.Tag | undefined;
685
+ for (const prop of cst.props || []) {
686
+ if ('type' in prop) {
687
+ const propTyped = prop as CstSourceToken;
688
+ if (propTyped.type === 'anchor') {
689
+ anchor = this.parseAnchorToken(propTyped.source, "");
690
+ } else if (propTyped.type === 'tag') {
691
+ tag = this.parseTagToken(propTyped.source, "");
692
+ }
693
+ }
694
+ }
695
+
696
+ return {
697
+ node: {
698
+ kind: Yaml.Kind.Scalar,
699
+ id: randomId(),
700
+ prefix: "",
701
+ markers: emptyMarkers,
702
+ style,
703
+ anchor,
704
+ tag,
705
+ value
706
+ },
707
+ trailing: "" // Block scalars don't have separate end tokens
708
+ };
709
+ }
710
+
711
+ private convertAliasWithTrailing(cst: CstFlowScalar): {node: Yaml.Alias, trailing: string} {
712
+ // Alias source is like "*anchorName"
713
+ const key = cst.source.substring(1); // Remove the *
714
+
715
+ const anchor: Yaml.Anchor = {
716
+ kind: Yaml.Kind.Anchor,
717
+ id: randomId(),
718
+ prefix: "",
719
+ markers: emptyMarkers,
720
+ postfix: "",
721
+ key
722
+ };
723
+
724
+ // End tokens contain trailing whitespace
725
+ const trailing = this.concatenateSources(cst.end || []);
726
+
727
+ return {
728
+ node: {
729
+ kind: Yaml.Kind.Alias,
730
+ id: randomId(),
731
+ prefix: "",
732
+ markers: emptyMarkers,
733
+ anchor
734
+ },
735
+ trailing
736
+ };
737
+ }
738
+
739
+ private parseAnchorToken(source: string, prefix: string): Yaml.Anchor {
740
+ // Anchor source is like "&anchorName"
741
+ const key = source.substring(1); // Remove the &
742
+
743
+ return {
744
+ kind: Yaml.Kind.Anchor,
745
+ id: randomId(),
746
+ prefix,
747
+ markers: emptyMarkers,
748
+ postfix: "",
749
+ key
750
+ };
751
+ }
752
+
753
+ private parseTagToken(source: string, prefix: string): Yaml.Tag {
754
+ return this.parseTagTokenWithSuffix(source, prefix, [], 0);
755
+ }
756
+
757
+ private parseTagTokenWithSuffix(source: string, prefix: string, remainingTokens: CstSourceToken[], startIndex: number): Yaml.Tag {
758
+ let name: string;
759
+ let tagKind: Yaml.TagKind;
760
+
761
+ if (source.startsWith('!<') && source.endsWith('>')) {
762
+ // Explicit global tag: !<tag:yaml.org,2002:str>
763
+ name = source.substring(2, source.length - 1);
764
+ tagKind = Yaml.TagKind.EXPLICIT_GLOBAL;
765
+ } else if (source.startsWith('!!')) {
766
+ // Implicit global tag: !!str
767
+ name = source.substring(2);
768
+ tagKind = Yaml.TagKind.IMPLICIT_GLOBAL;
769
+ } else {
770
+ // Local tag: !custom
771
+ name = source.substring(1);
772
+ tagKind = Yaml.TagKind.LOCAL;
773
+ }
774
+
775
+ // Collect remaining tokens as suffix (whitespace after the tag)
776
+ let suffix = "";
777
+ for (let i = startIndex; i < remainingTokens.length; i++) {
778
+ suffix += remainingTokens[i].source;
779
+ }
780
+
781
+ return {
782
+ kind: Yaml.Kind.Tag,
783
+ id: randomId(),
784
+ prefix,
785
+ markers: emptyMarkers,
786
+ name,
787
+ suffix,
788
+ tagKind
789
+ };
790
+ }
791
+
792
+ private createEmptyScalar(prefix: string = ""): Yaml.Scalar {
793
+ return {
794
+ kind: Yaml.Kind.Scalar,
795
+ id: randomId(),
796
+ prefix,
797
+ markers: emptyMarkers,
798
+ style: Yaml.ScalarStyle.PLAIN,
799
+ anchor: undefined,
800
+ tag: undefined,
801
+ value: ""
802
+ };
803
+ }
804
+
805
+ /**
806
+ * Prepends whitespace to a value node in the appropriate location based on its type.
807
+ * - For Scalars and Aliases: prepend to prefix
808
+ * - For flow Mappings (with braces): prepend to openingBracePrefix
809
+ * - For flow Sequences (with brackets): prepend to openingBracketPrefix
810
+ * - For block Mappings/Sequences: prepend to first entry's prefix
811
+ */
812
+ private prependWhitespaceToValue(value: Yaml.Block, whitespace: string): Yaml.Block {
813
+ if (value.kind === Yaml.Kind.Scalar || value.kind === Yaml.Kind.Alias) {
814
+ return {...value, prefix: whitespace + value.prefix};
815
+ }
816
+
817
+ if (value.kind === Yaml.Kind.Mapping) {
818
+ const mapping = value as Yaml.Mapping;
819
+ if (mapping.openingBracePrefix !== undefined) {
820
+ // Flow mapping: prepend to opening brace prefix
821
+ return {...mapping, openingBracePrefix: whitespace + mapping.openingBracePrefix};
822
+ } else if (mapping.entries.length > 0) {
823
+ // Block mapping: prepend to first entry's prefix
824
+ const firstEntry = mapping.entries[0];
825
+ const updatedFirstEntry = {...firstEntry, prefix: whitespace + firstEntry.prefix};
826
+ return {...mapping, entries: [updatedFirstEntry, ...mapping.entries.slice(1)]};
827
+ }
828
+ }
829
+
830
+ if (value.kind === Yaml.Kind.Sequence) {
831
+ const sequence = value as Yaml.Sequence;
832
+ if (sequence.openingBracketPrefix !== undefined) {
833
+ // Flow sequence: prepend to opening bracket prefix
834
+ return {...sequence, openingBracketPrefix: whitespace + sequence.openingBracketPrefix};
835
+ } else if (sequence.entries.length > 0) {
836
+ // Block sequence: prepend to first entry's prefix
837
+ const firstEntry = sequence.entries[0];
838
+ const updatedFirstEntry = {...firstEntry, prefix: whitespace + firstEntry.prefix};
839
+ return {...sequence, entries: [updatedFirstEntry, ...sequence.entries.slice(1)]};
840
+ }
841
+ }
842
+
843
+ // Fallback: prepend to prefix (shouldn't normally reach here)
844
+ return {...value, prefix: whitespace + value.prefix} as Yaml.Block;
845
+ }
846
+
847
+ private concatenateSources(tokens: CstSourceToken[]): string {
848
+ return tokens.map(t => t.source).join('');
849
+ }
850
+ }