@intentius/chant 0.98.0 → 0.100.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/dist/cli/commands/import.d.ts +20 -4
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +184 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +13 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +15 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/lexicon.d.ts +19 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lint/engine.d.ts +6 -1
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +10 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/file-declarable-limit.d.ts.map +1 -1
- package/dist/lint/rules/flat-declarations.d.ts.map +1 -1
- package/dist/lint/rules/no-unused-declarable.d.ts.map +1 -1
- package/dist/lint/rules/property-kind.d.ts +7 -0
- package/dist/lint/rules/property-kind.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +9 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/yaml.d.ts +28 -6
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import-layout.test.ts +142 -0
- package/src/cli/commands/import-no-parser.test.ts +1 -1
- package/src/cli/commands/import.test.ts +25 -0
- package/src/cli/commands/import.ts +158 -36
- package/src/cli/commands/lint.ts +21 -8
- package/src/deep-observation.test.ts +21 -0
- package/src/deep-observation.ts +9 -0
- package/src/import/embedded.test.ts +153 -0
- package/src/import/embedded.ts +376 -0
- package/src/import/generator.ts +14 -0
- package/src/import/parser.ts +17 -1
- package/src/lexicon.ts +21 -0
- package/src/lint/engine.ts +7 -0
- package/src/lint/rule.ts +10 -0
- package/src/lint/rules/file-declarable-limit.ts +11 -4
- package/src/lint/rules/flat-declarations.ts +6 -1
- package/src/lint/rules/no-unused-declarable.ts +59 -2
- package/src/lint/rules/property-kind.test.ts +99 -0
- package/src/lint/rules/property-kind.ts +56 -0
- package/src/workspace/conformance/index.mjs +1 -0
- package/src/workspace/conformance/index.ts +23 -1
- package/src/yaml.test.ts +192 -1
- package/src/yaml.ts +404 -250
package/src/yaml.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Lightweight YAML emitter and parser.
|
|
3
3
|
*
|
|
4
4
|
* Covers the subset of YAML used by Chant lexicons (scalars, block arrays,
|
|
5
|
-
* nested objects, tagged values
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* nested objects, block scalars, tagged values, anchors, aliases and merge
|
|
6
|
+
* keys). Not a full YAML implementation: flow collections are read only when
|
|
7
|
+
* they are JSON. `splitYAMLDocuments` splits a multi-document stream. The
|
|
8
|
+
* parser throws a `YAMLParseError` on a line it cannot place instead of
|
|
9
|
+
* dropping it (#2991).
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +157,29 @@ export interface ParseResult {
|
|
|
155
157
|
endIndex: number;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Input the parser cannot place (#2991). The parser used to skip such a line,
|
|
162
|
+
* or read it as a key of whatever mapping was open at the top level, so a
|
|
163
|
+
* mis-indented or unsupported construct came back as a quietly different
|
|
164
|
+
* document. Now it is an error naming the line.
|
|
165
|
+
*/
|
|
166
|
+
export class YAMLParseError extends Error {
|
|
167
|
+
/** 1-based line number within the text handed to the parser. */
|
|
168
|
+
readonly line: number;
|
|
169
|
+
|
|
170
|
+
constructor(line: number, message: string) {
|
|
171
|
+
super(`YAML line ${line}: ${message}`);
|
|
172
|
+
this.name = "YAMLParseError";
|
|
173
|
+
this.line = line;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A mapping key: a double-quoted or single-quoted scalar, which may hold a
|
|
179
|
+
* colon, or a plain run of text up to the first colon.
|
|
180
|
+
*/
|
|
181
|
+
const KEY = String.raw`("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^\s:"'][^:]*?)`;
|
|
182
|
+
|
|
158
183
|
/**
|
|
159
184
|
* `key: value` on a line of its own (`KEY_LINE`, capturing the indent) and the
|
|
160
185
|
* same after a sequence item's `- ` (`ITEM_KEY`).
|
|
@@ -173,14 +198,133 @@ export interface ParseResult {
|
|
|
173
198
|
* this file's own emitter had just written as a list of objects (#2013). By the
|
|
174
199
|
* same rule `- key:value` is the scalar `"key:value"`, not a mapping.
|
|
175
200
|
*/
|
|
176
|
-
const KEY_LINE =
|
|
177
|
-
const ITEM_KEY =
|
|
201
|
+
const KEY_LINE = new RegExp(String.raw`^(\s*)${KEY}:(?=\s|$)\s*(.*)$`);
|
|
202
|
+
const ITEM_KEY = new RegExp(String.raw`^${KEY}:(?=\s|$)\s*(.*)$`);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A block sequence entry: `- value`, or a dash alone on its line whose value
|
|
206
|
+
* is the block below it. `-1` and `---` are not entries.
|
|
207
|
+
*/
|
|
208
|
+
const SEQ_ENTRY = /^(\s*)-(?:([ \t]+)(.*))?$/;
|
|
209
|
+
|
|
210
|
+
/** A document start (`---`) or end (`...`) marker, optionally followed by a comment. */
|
|
211
|
+
const DOCUMENT_MARKER = /^(?:---|\.\.\.)(?:[ \t]+#.*)?[ \t]*$/;
|
|
212
|
+
|
|
213
|
+
function isBlankOrComment(line: string): boolean {
|
|
214
|
+
const t = line.trim();
|
|
215
|
+
return t === "" || t.startsWith("#");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The first line at or after `from` that holds content, or `lines.length`. */
|
|
219
|
+
function nextContentLine(lines: string[], from: number): number {
|
|
220
|
+
let i = from;
|
|
221
|
+
while (i < lines.length && isBlankOrComment(lines[i])) i++;
|
|
222
|
+
return i;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function indentOf(line: string): number {
|
|
226
|
+
return line.search(/\S/);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isSeqEntry(line: string): boolean {
|
|
230
|
+
return SEQ_ENTRY.test(line);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A key as written, with the quotes and escapes of a quoted key removed. */
|
|
234
|
+
function keyName(raw: string): string {
|
|
235
|
+
const key = raw.trim();
|
|
236
|
+
return key.startsWith('"') || key.startsWith("'") ? String(parseScalar(key)) : key;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Split text into lines for the parser: `\r\n` and `\r` become `\n`, and a
|
|
241
|
+
* document marker becomes a blank line. `parseYAML` has always read a
|
|
242
|
+
* `---`-led file (a compose file, a GitLab CI file) by skipping the marker;
|
|
243
|
+
* that is kept, now that a line the parser cannot place is an error.
|
|
244
|
+
*/
|
|
245
|
+
function toLines(content: string): string[] {
|
|
246
|
+
return content
|
|
247
|
+
.replace(/\r\n?/g, "\n")
|
|
248
|
+
.split("\n")
|
|
249
|
+
.map((line) => (DOCUMENT_MARKER.test(line) ? "" : line));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parse a whole document with `parse`, starting at its first content line
|
|
254
|
+
* `first`, and require it to account for every line (#2991). The recursive
|
|
255
|
+
* parsers stop at a line they cannot place and hand it back to their caller;
|
|
256
|
+
* one that reaches the top is unplaceable, and throws.
|
|
257
|
+
*/
|
|
258
|
+
function parseWhole(
|
|
259
|
+
lines: string[],
|
|
260
|
+
first: number,
|
|
261
|
+
parse: (lines: string[], startIndex: number, baseIndent: number) => ParseResult,
|
|
262
|
+
): unknown {
|
|
263
|
+
const outer = anchors;
|
|
264
|
+
anchors = new Map();
|
|
265
|
+
try {
|
|
266
|
+
const result = parse(lines, first, indentOf(lines[first]));
|
|
267
|
+
const rest = nextContentLine(lines, result.endIndex);
|
|
268
|
+
if (rest < lines.length) {
|
|
269
|
+
throw new YAMLParseError(
|
|
270
|
+
rest + 1,
|
|
271
|
+
`cannot place ${JSON.stringify(lines[rest].trim())} at column ${indentOf(lines[rest]) + 1}; ` +
|
|
272
|
+
"check its indentation, or the construct is one this parser does not read",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return result.value;
|
|
276
|
+
} finally {
|
|
277
|
+
anchors = outer;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The anchors (`&name`) seen so far in the document being parsed, for its
|
|
283
|
+
* aliases (`*name`) and merge keys (`<<: *name`). GitLab CI templates and
|
|
284
|
+
* Alertmanager configs use all three; before #2991 an anchored block was
|
|
285
|
+
* hoisted into its parent and an alias read as the string `"*name"`.
|
|
286
|
+
* `parseWhole` gives each document its own map.
|
|
287
|
+
*/
|
|
288
|
+
let anchors = new Map<string, unknown>();
|
|
289
|
+
|
|
290
|
+
const ANCHOR = /^&([^\s,[\]{}]+)(?:[ \t]+(.*))?$/;
|
|
291
|
+
const ALIAS = /^\*([^\s,[\]{}]+)(?:[ \t]+#.*)?$/;
|
|
292
|
+
/** A flow sequence of aliases, the usual way to merge several anchors: `<<: [*a, *b]`. */
|
|
293
|
+
const ALIAS_LIST = /^\[\s*\*[^\s,[\]{}]+(?:\s*,\s*\*[^\s,[\]{}]+)*\s*\](?:[ \t]+#.*)?$/;
|
|
294
|
+
|
|
295
|
+
function resolveAlias(name: string, line: number): unknown {
|
|
296
|
+
if (!anchors.has(name)) throw new YAMLParseError(line, `the alias *${name} has no anchor &${name} before it`);
|
|
297
|
+
return structuredClone(anchors.get(name));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The merge key: its value's mappings fill in the keys a mapping does not set itself. */
|
|
301
|
+
const MERGE_KEY = "<<";
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Set `key` on a mapping under construction. A merge key's sources are held
|
|
305
|
+
* in `merges` and applied by {@link applyMerges} once the mapping is
|
|
306
|
+
* complete, because a key the mapping sets itself wins wherever it appears.
|
|
307
|
+
*/
|
|
308
|
+
function setKey(obj: Record<string, unknown>, merges: unknown[], key: string, value: unknown): void {
|
|
309
|
+
if (key === MERGE_KEY) merges.push(...(Array.isArray(value) ? value : [value]));
|
|
310
|
+
else obj[key] = value;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function applyMerges(obj: Record<string, unknown>, merges: unknown[], line: number): void {
|
|
314
|
+
for (const source of merges) {
|
|
315
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) {
|
|
316
|
+
throw new YAMLParseError(line, "a merge key (<<) takes a mapping or a list of mappings");
|
|
317
|
+
}
|
|
318
|
+
for (const [k, v] of Object.entries(source)) if (!(k in obj)) obj[k] = v;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
178
321
|
|
|
179
322
|
/**
|
|
180
323
|
* Parse a YAML document (or JSON document) into a plain object.
|
|
181
324
|
*
|
|
182
325
|
* Tries `JSON.parse` first; falls back to a line-based YAML parser that
|
|
183
|
-
* handles the subset of YAML commonly found in CI configuration files.
|
|
326
|
+
* handles the subset of YAML commonly found in CI configuration files. Throws
|
|
327
|
+
* {@link YAMLParseError} on a line it cannot place rather than dropping it.
|
|
184
328
|
*/
|
|
185
329
|
export function parseYAML(content: string): Record<string, unknown> {
|
|
186
330
|
try {
|
|
@@ -189,8 +333,12 @@ export function parseYAML(content: string): Record<string, unknown> {
|
|
|
189
333
|
// Fall through to YAML parsing
|
|
190
334
|
}
|
|
191
335
|
|
|
192
|
-
const lines = content
|
|
193
|
-
|
|
336
|
+
const lines = toLines(content);
|
|
337
|
+
const first = nextContentLine(lines, 0);
|
|
338
|
+
// A document with no content, or (see parseYAMLDocument) a top-level list,
|
|
339
|
+
// reads as an empty mapping; the callers rely on getting a mapping back.
|
|
340
|
+
if (first === lines.length || isSeqEntry(lines[first])) return {};
|
|
341
|
+
return parseWhole(lines, first, parseYAMLLines) as Record<string, unknown>;
|
|
194
342
|
}
|
|
195
343
|
|
|
196
344
|
/**
|
|
@@ -207,17 +355,12 @@ export function parseYAMLDocument(content: string): unknown {
|
|
|
207
355
|
// Fall through to YAML parsing
|
|
208
356
|
}
|
|
209
357
|
|
|
210
|
-
const lines = content
|
|
211
|
-
const first = lines
|
|
212
|
-
if (first
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
return parseYAMLLines(lines, 0, 0).value;
|
|
358
|
+
const lines = toLines(content);
|
|
359
|
+
const first = nextContentLine(lines, 0);
|
|
360
|
+
if (first === lines.length) return {};
|
|
361
|
+
return parseWhole(lines, first, isSeqEntry(lines[first]) ? parseYAMLArray : parseYAMLLines);
|
|
216
362
|
}
|
|
217
363
|
|
|
218
|
-
/** A document start (`---`) or end (`...`) marker, optionally followed by a comment. */
|
|
219
|
-
const DOCUMENT_MARKER = /^(?:---|\.\.\.)(?:[ \t]+#.*)?[ \t]*$/;
|
|
220
|
-
|
|
221
364
|
/**
|
|
222
365
|
* Split a YAML stream into its documents (#2965). A line holding only `---`
|
|
223
366
|
* starts a document and a line holding only `...` ends one; either may carry
|
|
@@ -321,7 +464,13 @@ function parseBlockScalar(
|
|
|
321
464
|
}
|
|
322
465
|
|
|
323
466
|
/**
|
|
324
|
-
* Parse indentation-based YAML lines into a key-value object
|
|
467
|
+
* Parse indentation-based YAML lines into a key-value object: the mapping
|
|
468
|
+
* whose keys sit at column `baseIndent`, starting at `startIndex`.
|
|
469
|
+
*
|
|
470
|
+
* It ends at the first line that is not one of its keys: a line at another
|
|
471
|
+
* column, a sequence entry, or text that is not `key:`. That line goes back
|
|
472
|
+
* to the caller in `endIndex`; if no caller owns it, `parseYAML` reports it
|
|
473
|
+
* (#2991). This function never skips a content line.
|
|
325
474
|
*/
|
|
326
475
|
export function parseYAMLLines(
|
|
327
476
|
lines: string[],
|
|
@@ -329,190 +478,236 @@ export function parseYAMLLines(
|
|
|
329
478
|
baseIndent: number,
|
|
330
479
|
): ParseResult {
|
|
331
480
|
const result: Record<string, unknown> = {};
|
|
481
|
+
const merges: unknown[] = [];
|
|
332
482
|
let i = startIndex;
|
|
333
483
|
|
|
334
484
|
while (i < lines.length) {
|
|
335
485
|
const line = lines[i];
|
|
336
|
-
|
|
337
|
-
if (line.trim() === "" || line.trim().startsWith("#")) {
|
|
486
|
+
if (isBlankOrComment(line)) {
|
|
338
487
|
i++;
|
|
339
488
|
continue;
|
|
340
489
|
}
|
|
341
|
-
|
|
342
|
-
const indent = line.search(/\S/);
|
|
343
|
-
if (indent < baseIndent) break; // Dedented — done with this block
|
|
344
|
-
if (indent > baseIndent && startIndex > 0) break; // Unexpected indent
|
|
345
|
-
|
|
490
|
+
if (indentOf(line) !== baseIndent || isSeqEntry(line)) break;
|
|
346
491
|
const keyMatch = line.match(KEY_LINE);
|
|
347
|
-
if (keyMatch)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (inlineValue === "" || inlineValue.startsWith("#")) {
|
|
352
|
-
// Check next line for array or nested object
|
|
353
|
-
if (i + 1 < lines.length) {
|
|
354
|
-
const nextLine = lines[i + 1];
|
|
355
|
-
const nextIndent = nextLine.search(/\S/);
|
|
356
|
-
if (nextLine.trimStart().startsWith("- ") && nextIndent >= indent) {
|
|
357
|
-
// Same-indent arrays are valid YAML (e.g. controller-gen output):
|
|
358
|
-
// versions:
|
|
359
|
-
// - name: v1
|
|
360
|
-
const arr = parseYAMLArray(lines, i + 1, nextIndent);
|
|
361
|
-
result[key] = arr.value;
|
|
362
|
-
i = arr.endIndex;
|
|
363
|
-
continue;
|
|
364
|
-
} else if (nextIndent > indent) {
|
|
365
|
-
const nested = parseYAMLLines(lines, i + 1, nextIndent);
|
|
366
|
-
result[key] = nested.value;
|
|
367
|
-
i = nested.endIndex;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
result[key] = null;
|
|
372
|
-
i++;
|
|
373
|
-
} else if (inlineValue.startsWith("[")) {
|
|
374
|
-
// Inline array
|
|
375
|
-
try {
|
|
376
|
-
result[key] = JSON.parse(inlineValue);
|
|
377
|
-
} catch {
|
|
378
|
-
result[key] = inlineValue;
|
|
379
|
-
}
|
|
380
|
-
i++;
|
|
381
|
-
} else if (inlineValue.startsWith("{")) {
|
|
382
|
-
// Inline object
|
|
383
|
-
try {
|
|
384
|
-
result[key] = JSON.parse(inlineValue);
|
|
385
|
-
} catch {
|
|
386
|
-
result[key] = inlineValue;
|
|
387
|
-
}
|
|
388
|
-
i++;
|
|
389
|
-
} else {
|
|
390
|
-
const header = blockScalarHeader(inlineValue);
|
|
391
|
-
if (header) {
|
|
392
|
-
const block = parseBlockScalar(lines, i + 1, indent, header);
|
|
393
|
-
result[key] = block.value;
|
|
394
|
-
i = block.endIndex;
|
|
395
|
-
} else {
|
|
396
|
-
result[key] = parseScalar(inlineValue);
|
|
397
|
-
i++;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
} else if (line.trimStart().startsWith("- ")) {
|
|
401
|
-
break;
|
|
402
|
-
} else {
|
|
403
|
-
i++;
|
|
404
|
-
}
|
|
492
|
+
if (!keyMatch) break;
|
|
493
|
+
const value = parseValue(keyMatch[3].trim(), lines, i, baseIndent);
|
|
494
|
+
setKey(result, merges, keyName(keyMatch[2]), value.value);
|
|
495
|
+
i = value.endIndex;
|
|
405
496
|
}
|
|
497
|
+
applyMerges(result, merges, startIndex + 1);
|
|
406
498
|
|
|
407
499
|
return { value: result, endIndex: i };
|
|
408
500
|
}
|
|
409
501
|
|
|
410
502
|
/**
|
|
411
|
-
* Parse the value of
|
|
412
|
-
*
|
|
503
|
+
* Parse the value of the key on line `at`, whose text after the colon is
|
|
504
|
+
* `inline` and whose own column is `keyIndent`. Used for mapping keys and for
|
|
505
|
+
* the keys of a sequence item alike.
|
|
506
|
+
*
|
|
507
|
+
* An empty inline value (or only a comment) means the value is the block on
|
|
508
|
+
* the next content line. Blank, whitespace-only and comment lines between the
|
|
509
|
+
* key and that block do not end it (#2991): Helm renders an empty `{{ if }}`
|
|
510
|
+
* as exactly such a line, and treating it as the end of the value turned
|
|
511
|
+
*
|
|
512
|
+
* spec:
|
|
513
|
+
*
|
|
514
|
+
* serviceAccountName: collector
|
|
515
|
+
*
|
|
516
|
+
* into `spec: null` with `serviceAccountName` hoisted to the parent.
|
|
517
|
+
*
|
|
518
|
+
* The two nested shapes do NOT share a threshold (#1311), except below a
|
|
519
|
+
* sequence entry (`sequenceAtKeyColumn` false), where a `-` at the entry's
|
|
520
|
+
* own column is the next entry:
|
|
521
|
+
*
|
|
522
|
+
* - a SEQUENCE may sit at the key's own column (valid YAML, and what
|
|
523
|
+
* kubectl and Kubernetes manifests emit);
|
|
524
|
+
* - a MAPPING must be indented past it, otherwise the next line is a
|
|
525
|
+
* sibling key and this key's value is null:
|
|
526
|
+
*
|
|
527
|
+
* - name: a
|
|
528
|
+
* meta: <- no value
|
|
529
|
+
* other: b <- a sibling, NOT meta's content
|
|
413
530
|
*/
|
|
414
|
-
function
|
|
415
|
-
|
|
531
|
+
function parseValue(
|
|
532
|
+
inline: string,
|
|
416
533
|
lines: string[],
|
|
417
|
-
|
|
534
|
+
at: number,
|
|
418
535
|
keyIndent: number,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
536
|
+
sequenceAtKeyColumn = true,
|
|
537
|
+
): ParseResult {
|
|
538
|
+
const anchor = inline.match(ANCHOR);
|
|
539
|
+
if (anchor) {
|
|
540
|
+
const value = parseValue((anchor[2] ?? "").trim(), lines, at, keyIndent, sequenceAtKeyColumn);
|
|
541
|
+
anchors.set(anchor[1], structuredClone(value.value));
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
const alias = inline.match(ALIAS);
|
|
545
|
+
if (alias) return { value: resolveAlias(alias[1], at + 1), endIndex: at + 1 };
|
|
546
|
+
if (ALIAS_LIST.test(inline)) {
|
|
547
|
+
const names = [...inline.matchAll(/\*([^\s,[\]{}]+)/g)].map((m) => m[1]);
|
|
548
|
+
return { value: names.map((name) => resolveAlias(name, at + 1)), endIndex: at + 1 };
|
|
549
|
+
}
|
|
550
|
+
if (inline === "" || inline.startsWith("#")) {
|
|
551
|
+
const k = nextContentLine(lines, at + 1);
|
|
552
|
+
if (k < lines.length) {
|
|
553
|
+
const ni = indentOf(lines[k]);
|
|
554
|
+
const nested = isSeqEntry(lines[k]) && sequenceAtKeyColumn ? ni >= keyIndent : ni > keyIndent;
|
|
555
|
+
if (nested) return parseNode(lines, k, keyIndent);
|
|
432
556
|
}
|
|
433
|
-
|
|
434
|
-
|
|
557
|
+
return { value: null, endIndex: at + 1 };
|
|
558
|
+
}
|
|
559
|
+
if (inline.startsWith("[") || inline.startsWith("{")) {
|
|
560
|
+
// Inline array or object
|
|
561
|
+
try {
|
|
562
|
+
return { value: JSON.parse(inline), endIndex: at + 1 };
|
|
563
|
+
} catch {
|
|
564
|
+
return { value: inline, endIndex: at + 1 };
|
|
435
565
|
}
|
|
436
|
-
return parseScalar(inlineValue);
|
|
437
566
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
567
|
+
const header = blockScalarHeader(inline);
|
|
568
|
+
if (header) return parseBlockScalar(lines, at + 1, keyIndent, header);
|
|
569
|
+
return parseFlowScalar(inline, lines, at + 1, keyIndent);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Parse the node that starts on content line `k`, below a key or a bare `-`
|
|
574
|
+
* at column `parentIndent`: a sequence, a mapping, or a scalar written on the
|
|
575
|
+
* line after its key.
|
|
576
|
+
*/
|
|
577
|
+
function parseNode(lines: string[], k: number, parentIndent: number): ParseResult {
|
|
578
|
+
const line = lines[k];
|
|
579
|
+
if (isSeqEntry(line)) return parseYAMLArray(lines, k, indentOf(line));
|
|
580
|
+
if (KEY_LINE.test(line)) return parseYAMLLines(lines, k, indentOf(line));
|
|
581
|
+
return parseValue(line.trim(), lines, k, parentIndent);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The index of the quote that closes a quoted scalar opened by `quote`,
|
|
586
|
+
* scanning `text` from `from`, or -1. A double-quoted scalar escapes with a
|
|
587
|
+
* backslash, a single-quoted one by doubling the quote.
|
|
588
|
+
*/
|
|
589
|
+
function closingQuote(text: string, quote: string, from: number): number {
|
|
590
|
+
for (let i = from; i < text.length; i++) {
|
|
591
|
+
const ch = text[i];
|
|
592
|
+
if (quote === '"' && ch === "\\") {
|
|
593
|
+
i++;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (ch === quote) {
|
|
597
|
+
if (quote === "'" && text[i + 1] === "'") {
|
|
598
|
+
i++;
|
|
599
|
+
continue;
|
|
461
600
|
}
|
|
601
|
+
return i;
|
|
462
602
|
}
|
|
463
603
|
}
|
|
464
|
-
return
|
|
604
|
+
return -1;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Whether `text` is exactly one quoted scalar, e.g. `"80:80"`. */
|
|
608
|
+
function isWholeQuotedScalar(text: string): boolean {
|
|
609
|
+
const quote = text[0];
|
|
610
|
+
if (quote !== '"' && quote !== "'") return false;
|
|
611
|
+
return closingQuote(text, quote, 1) === text.length - 1;
|
|
465
612
|
}
|
|
466
613
|
|
|
467
614
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
615
|
+
* Parse a plain or quoted scalar whose first line is `first` and whose
|
|
616
|
+
* continuation lines, if any, start at `from` and are indented past
|
|
617
|
+
* `parentIndent`. YAML lets both kinds span lines, and go-yaml (so Helm's
|
|
618
|
+
* `toYaml`) wraps long strings that way. Lines fold into one: a line break
|
|
619
|
+
* becomes a space, and each blank line in between becomes a newline.
|
|
472
620
|
*
|
|
473
|
-
*
|
|
474
|
-
* belongs to it and anything at the key's own column is a sibling;
|
|
475
|
-
* - a nested SEQUENCE may sit at the SAME column as its key, which is valid
|
|
476
|
-
* YAML and what kubectl and Kubernetes manifests both emit:
|
|
477
|
-
*
|
|
478
|
-
* - name: web
|
|
479
|
-
* ports:
|
|
480
|
-
* - containerPort: 80
|
|
481
|
-
* env: <- a sibling, at `ports`' own column
|
|
482
|
-
*
|
|
483
|
-
* An indent rule cannot separate those, so the sequence is re-parsed to
|
|
484
|
-
* find where it ends. `parseYAMLArray` already reports that as `endIndex`.
|
|
621
|
+
* These continuation lines used to be skipped, keeping only the first line.
|
|
485
622
|
*/
|
|
486
|
-
function
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
623
|
+
function parseFlowScalar(first: string, lines: string[], from: number, parentIndent: number): ParseResult {
|
|
624
|
+
const quote = first[0];
|
|
625
|
+
if (quote === '"' || quote === "'") {
|
|
626
|
+
if (closingQuote(first, quote, 1) !== -1) return { value: parseScalar(first), endIndex: from };
|
|
627
|
+
return parseMultilineQuoted(first, lines, from, parentIndent, quote);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
let text = first;
|
|
631
|
+
let endIndex = from;
|
|
632
|
+
let blanks = 0;
|
|
633
|
+
for (let j = from; j < lines.length; j++) {
|
|
634
|
+
const line = lines[j];
|
|
635
|
+
const t = line.trim();
|
|
636
|
+
if (t === "") {
|
|
637
|
+
blanks++;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// A comment ends a plain scalar; so does a line back at the parent's column.
|
|
641
|
+
if (t.startsWith("#") || indentOf(line) <= parentIndent) break;
|
|
642
|
+
if (KEY_LINE.test(line)) {
|
|
643
|
+
throw new YAMLParseError(
|
|
644
|
+
j + 1,
|
|
645
|
+
/^!\S*$/.test(first)
|
|
646
|
+
? `the tag ${JSON.stringify(first)} before a nested block is not supported`
|
|
647
|
+
: `${JSON.stringify(t)} is indented under the value ${JSON.stringify(first)}, ` +
|
|
648
|
+
"and a scalar cannot hold a mapping; check its indentation",
|
|
649
|
+
);
|
|
493
650
|
}
|
|
651
|
+
text += blanks > 0 ? "\n".repeat(blanks) : " ";
|
|
652
|
+
text += t;
|
|
653
|
+
blanks = 0;
|
|
654
|
+
endIndex = j + 1;
|
|
494
655
|
}
|
|
495
|
-
return
|
|
656
|
+
return { value: parseScalar(text), endIndex };
|
|
496
657
|
}
|
|
497
658
|
|
|
498
659
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
660
|
+
* The rest of a quoted scalar left open on its first line: lines up to the
|
|
661
|
+
* closing quote, folded as {@link parseFlowScalar} describes. For a
|
|
662
|
+
* double-quoted scalar a line ending in `\` is an escaped line break that
|
|
663
|
+
* joins the next line with nothing between; it is kept as `\` + newline for
|
|
664
|
+
* `unescapeDoubleQuoted`, which removes it with the indentation after it.
|
|
501
665
|
*/
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
666
|
+
function parseMultilineQuoted(
|
|
667
|
+
first: string,
|
|
668
|
+
lines: string[],
|
|
669
|
+
from: number,
|
|
670
|
+
parentIndent: number,
|
|
671
|
+
quote: string,
|
|
672
|
+
): ParseResult {
|
|
673
|
+
const segments = [first.slice(1)];
|
|
674
|
+
for (let j = from; j < lines.length; j++) {
|
|
675
|
+
const line = lines[j];
|
|
676
|
+
if (line.trim() !== "" && indentOf(line) <= parentIndent) break;
|
|
677
|
+
const close = closingQuote(line, quote, 0);
|
|
678
|
+
if (close === -1) {
|
|
679
|
+
segments.push(line);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
segments.push(line.slice(0, close));
|
|
683
|
+
let body = "";
|
|
684
|
+
let blanks = 0;
|
|
685
|
+
let escapedBreak = false;
|
|
686
|
+
segments.forEach((raw, n) => {
|
|
687
|
+
const isFirst = n === 0;
|
|
688
|
+
const isLast = n === segments.length - 1;
|
|
689
|
+
let s = raw;
|
|
690
|
+
if (!isFirst && !escapedBreak) s = s.replace(/^[ \t]+/, "");
|
|
691
|
+
const endsEscaped = !isLast && quote === '"' && /(?:^|[^\\])(?:\\\\)*\\$/.test(s);
|
|
692
|
+
if (!isLast && !endsEscaped) s = s.replace(/[ \t]+$/, "");
|
|
693
|
+
if (!isFirst && !isLast && s === "" && !escapedBreak) {
|
|
694
|
+
blanks++;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (!isFirst) body += escapedBreak ? "\n" : blanks > 0 ? "\n".repeat(blanks) : " ";
|
|
698
|
+
body += s;
|
|
699
|
+
blanks = 0;
|
|
700
|
+
escapedBreak = endsEscaped;
|
|
701
|
+
});
|
|
702
|
+
return { value: parseScalar(quote + body + quote), endIndex: j + 1 };
|
|
510
703
|
}
|
|
511
|
-
|
|
704
|
+
throw new YAMLParseError(from, `unterminated ${quote === '"' ? "double" : "single"}-quoted scalar`);
|
|
512
705
|
}
|
|
513
706
|
|
|
514
707
|
/**
|
|
515
|
-
* Parse a block array
|
|
708
|
+
* Parse a block array: the `- ` entries at column `baseIndent`, starting at
|
|
709
|
+
* `startIndex`. Like {@link parseYAMLLines} it ends at the first line that is
|
|
710
|
+
* not one of its entries and hands that line back in `endIndex`.
|
|
516
711
|
*/
|
|
517
712
|
export function parseYAMLArray(
|
|
518
713
|
lines: string[],
|
|
@@ -524,98 +719,57 @@ export function parseYAMLArray(
|
|
|
524
719
|
|
|
525
720
|
while (i < lines.length) {
|
|
526
721
|
const line = lines[i];
|
|
527
|
-
if (
|
|
722
|
+
if (isBlankOrComment(line)) {
|
|
528
723
|
i++;
|
|
529
724
|
continue;
|
|
530
725
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// the key's column itself also swallowed the item's sibling keys,
|
|
555
|
-
// which sit at exactly that column:
|
|
556
|
-
//
|
|
557
|
-
// - context: <- key at column 2
|
|
558
|
-
// cluster: c1 <- its block, column 4
|
|
559
|
-
// name: n1 <- a SIBLING at column 2, was skipped
|
|
560
|
-
//
|
|
561
|
-
// Only the item's first key was affected: the sibling loop below
|
|
562
|
-
// already skips past its own key's column, and the block-scalar
|
|
563
|
-
// branch immediately below has always used `+ 1` for this reason.
|
|
564
|
-
? skipValueBlock(lines, i + 1, nextIndent)
|
|
565
|
-
: blockScalarHeader(firstVal)
|
|
566
|
-
// Block body is indented past the key (nextIndent); skip it (#910).
|
|
567
|
-
? skipNestedBlock(lines, i + 1, nextIndent + 1)
|
|
568
|
-
: i + 1;
|
|
569
|
-
while (j < lines.length) {
|
|
570
|
-
const nextLine = lines[j];
|
|
571
|
-
if (nextLine.trim() === "" || nextLine.trim().startsWith("#")) {
|
|
572
|
-
j++;
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
const ni = nextLine.search(/\S/);
|
|
576
|
-
if (ni < nextIndent) break;
|
|
577
|
-
if (ni > nextIndent) break; // belongs to a nested block already consumed
|
|
578
|
-
const nextKV = nextLine.match(KEY_LINE);
|
|
579
|
-
if (nextKV) {
|
|
580
|
-
const nextVal = nextKV[3].trim();
|
|
581
|
-
obj[nextKV[2].trim()] = parseArrayItemValue(nextVal, lines, j, ni);
|
|
582
|
-
if (nextVal === "" || nextVal.startsWith("#")) {
|
|
583
|
-
// Same rule as the first key above: past this key's own column,
|
|
584
|
-
// or to the end of a same-column sequence (#1311).
|
|
585
|
-
j = skipValueBlock(lines, j + 1, ni);
|
|
586
|
-
} else if (blockScalarHeader(nextVal)) {
|
|
587
|
-
// Skip the block body (indented past this key at `ni`) (#910).
|
|
588
|
-
j = skipNestedBlock(lines, j + 1, ni + 1);
|
|
589
|
-
} else {
|
|
590
|
-
j++;
|
|
591
|
-
}
|
|
592
|
-
} else {
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
result.push(obj);
|
|
597
|
-
i = j;
|
|
598
|
-
} else {
|
|
599
|
-
const header = blockScalarHeader(itemValue);
|
|
600
|
-
if (header) {
|
|
601
|
-
// A block scalar as the item itself (`- |`). Without this branch the
|
|
602
|
-
// header parsed as the literal string "|" and the body lines leaked
|
|
603
|
-
// into whatever came next — inside a container list that hoisted the
|
|
604
|
-
// sibling keys after `args:` (securityContext, even the following
|
|
605
|
-
// `containers:` key) to the document root, so post-synth checks read
|
|
606
|
-
// a manifest that had lost them (#1482). The body is indented past
|
|
607
|
-
// the dash's column.
|
|
608
|
-
const block = parseBlockScalar(lines, i + 1, indent, header);
|
|
609
|
-
result.push(block.value);
|
|
610
|
-
i = block.endIndex;
|
|
611
|
-
} else {
|
|
612
|
-
result.push(parseScalar(itemValue));
|
|
613
|
-
i++;
|
|
726
|
+
const itemMatch = line.match(SEQ_ENTRY);
|
|
727
|
+
if (!itemMatch || indentOf(line) !== baseIndent) break;
|
|
728
|
+
const itemValue = (itemMatch[3] ?? "").trim();
|
|
729
|
+
|
|
730
|
+
// The column the entry's content starts at, which is where the keys of a
|
|
731
|
+
// mapping entry sit: two past the dash for `- key`, more for `- key`.
|
|
732
|
+
const keyIndent = baseIndent + 1 + (itemMatch[2]?.length ?? 1);
|
|
733
|
+
// A quoted scalar holding a colon (e.g. "80:80") is not a key-value pair.
|
|
734
|
+
const kvMatch = isWholeQuotedScalar(itemValue) ? null : itemValue.match(ITEM_KEY);
|
|
735
|
+
if (kvMatch) {
|
|
736
|
+
const obj: Record<string, unknown> = {};
|
|
737
|
+
const merges: unknown[] = [];
|
|
738
|
+
const firstValue = parseValue(kvMatch[2].trim(), lines, i, keyIndent);
|
|
739
|
+
setKey(obj, merges, keyName(kvMatch[1]), firstValue.value);
|
|
740
|
+
// The entry's other keys, at the first key's column. Each value's
|
|
741
|
+
// parse reports where it ended, nested block, same-column sequence
|
|
742
|
+
// (#1311) and block scalar (#910) alike.
|
|
743
|
+
let j = firstValue.endIndex;
|
|
744
|
+
while (j < lines.length) {
|
|
745
|
+
const nextLine = lines[j];
|
|
746
|
+
if (isBlankOrComment(nextLine)) {
|
|
747
|
+
j++;
|
|
748
|
+
continue;
|
|
614
749
|
}
|
|
750
|
+
if (indentOf(nextLine) !== keyIndent || isSeqEntry(nextLine)) break;
|
|
751
|
+
const nextKV = nextLine.match(KEY_LINE);
|
|
752
|
+
if (!nextKV) break;
|
|
753
|
+
const value = parseValue(nextKV[3].trim(), lines, j, keyIndent);
|
|
754
|
+
setKey(obj, merges, keyName(nextKV[2]), value.value);
|
|
755
|
+
j = value.endIndex;
|
|
615
756
|
}
|
|
616
|
-
|
|
617
|
-
|
|
757
|
+
applyMerges(obj, merges, i + 1);
|
|
758
|
+
result.push(obj);
|
|
759
|
+
i = j;
|
|
760
|
+
continue;
|
|
618
761
|
}
|
|
762
|
+
|
|
763
|
+
// Anything else is read the way a mapping value is: a scalar, an inline
|
|
764
|
+
// list or object, an anchor or alias, a block scalar, or, for a dash alone
|
|
765
|
+
// on its line (#1286), the block below it after any blank or comment
|
|
766
|
+
// lines, or null. A block scalar's body (`- |`) is indented past the
|
|
767
|
+
// dash's column; before #1482 the header parsed as the literal string "|"
|
|
768
|
+
// and the body lines leaked into whatever came next, hoisting a
|
|
769
|
+
// container's keys after `args:` to the document root.
|
|
770
|
+
const item = parseValue(itemValue, lines, i, baseIndent, false);
|
|
771
|
+
result.push(item.value);
|
|
772
|
+
i = item.endIndex;
|
|
619
773
|
}
|
|
620
774
|
|
|
621
775
|
return { value: result, endIndex: i };
|