@intentius/chant 0.97.0 → 0.99.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 +67 -4
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/tools/import.d.ts +4 -0
- package/dist/cli/mcp/tools/import.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +10 -0
- package/dist/cli/plugins.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 +44 -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 +70 -0
- package/src/cli/commands/import.test.ts +284 -2
- package/src/cli/commands/import.ts +325 -86
- package/src/cli/commands/lint.ts +21 -8
- package/src/cli/handlers/misc.ts +3 -0
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +19 -11
- package/src/cli/handlers/operator.ts +17 -3
- package/src/cli/main.ts +3 -1
- package/src/cli/mcp/tools/import.ts +6 -0
- package/src/cli/plugins.ts +41 -2
- 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 +244 -1
- package/src/yaml.ts +443 -241
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import { flatDeclarationsRule } from "./flat-declarations";
|
|
4
|
+
import { noUnusedDeclarableRule } from "./no-unused-declarable";
|
|
5
|
+
import { fileDeclarableLimitRule } from "./file-declarable-limit";
|
|
6
|
+
import type { LintContext } from "../rule";
|
|
7
|
+
|
|
8
|
+
// chant #2957: a dashboard-shaped file whose panels, rows, queries and
|
|
9
|
+
// variables are property-kind declarables, as the grafana lexicon declares them.
|
|
10
|
+
const PROPERTY = new Set(["Row", "StatPanel", "TimeSeriesPanel", "PromQuery", "QueryVariable"]);
|
|
11
|
+
|
|
12
|
+
const DASHBOARD = `
|
|
13
|
+
import { Dashboard, Row, TimeSeriesPanel as Series, StatPanel, PromQuery, QueryVariable } from "@intentius/chant-lexicon-grafana";
|
|
14
|
+
|
|
15
|
+
export const service = new QueryVariable({ name: "service", query: "label_values(up, job)" });
|
|
16
|
+
|
|
17
|
+
export const overview = new Dashboard({
|
|
18
|
+
title: "Overview",
|
|
19
|
+
variables: [service],
|
|
20
|
+
panels: [
|
|
21
|
+
new Row({ title: "RED", panels: [
|
|
22
|
+
new StatPanel({ title: "Errors", targets: [new PromQuery({ expr: "a" })], fieldConfig: { defaults: { unit: "percentunit" } }, options: { graphMode: "area" } }),
|
|
23
|
+
new Series({ title: "Rate", targets: [new PromQuery({ expr: "b" }), new PromQuery({ expr: "c" })], fieldConfig: { defaults: { unit: "reqps" } } }),
|
|
24
|
+
new Series({ title: "Latency", targets: [new PromQuery({ expr: "d" })], fieldConfig: { defaults: { unit: "ms" } } }),
|
|
25
|
+
new Series({ title: "Saturation", targets: [new PromQuery({ expr: "e" })] }),
|
|
26
|
+
] }),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function context(code: string, propertyClasses?: ReadonlySet<string>): LintContext {
|
|
32
|
+
return {
|
|
33
|
+
sourceFile: ts.createSourceFile("dashboard.ts", code, ts.ScriptTarget.Latest, true),
|
|
34
|
+
entities: [],
|
|
35
|
+
filePath: "dashboard.ts",
|
|
36
|
+
propertyClasses,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("property-kind declarables in COR001, COR004 and COR009 (chant #2957)", () => {
|
|
41
|
+
test("without property classes the rules count every declarable, as before", () => {
|
|
42
|
+
const ctx = context(DASHBOARD);
|
|
43
|
+
expect(fileDeclarableLimitRule.check(ctx)).toHaveLength(1);
|
|
44
|
+
expect(fileDeclarableLimitRule.check(ctx)[0].message).toContain("12 Declarable instances");
|
|
45
|
+
expect(flatDeclarationsRule.check(ctx)).toHaveLength(4);
|
|
46
|
+
expect(noUnusedDeclarableRule.check(ctx).map((d) => d.message)).toEqual([
|
|
47
|
+
expect.stringContaining("'overview'"),
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("with them, the dashboard file is clean", () => {
|
|
52
|
+
const ctx = context(DASHBOARD, PROPERTY);
|
|
53
|
+
expect(fileDeclarableLimitRule.check(ctx)).toEqual([]);
|
|
54
|
+
expect(flatDeclarationsRule.check(ctx)).toEqual([]);
|
|
55
|
+
expect(noUnusedDeclarableRule.check(ctx)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("COR009 still counts resources, and only resources", () => {
|
|
59
|
+
const resources = Array.from({ length: 9 }, (_, i) => `export const d${i} = new Dashboard({ panels: [new Row({})] });`).join("\n");
|
|
60
|
+
const [diag] = fileDeclarableLimitRule.check(context(resources, PROPERTY));
|
|
61
|
+
expect(diag.message).toContain("9 Declarable instances");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("COR001 still flags inline objects on a resource", () => {
|
|
65
|
+
const code = `export const d = new Dashboard({ time: { from: "now-1h", to: "now" }, panels: [new StatPanel({ options: { a: 1 } })] });`;
|
|
66
|
+
const diags = flatDeclarationsRule.check(context(code, PROPERTY));
|
|
67
|
+
expect(diags).toHaveLength(1);
|
|
68
|
+
expect(diags[0].column).toBe(code.indexOf("{ from") + 1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("COR004 skips an exported property-kind declarable used from another file", () => {
|
|
72
|
+
const code = `export const latency = new TimeSeriesPanel({ title: "Latency" });`;
|
|
73
|
+
expect(noUnusedDeclarableRule.check(context(code, PROPERTY))).toEqual([]);
|
|
74
|
+
expect(noUnusedDeclarableRule.check(context(code))).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("COR004 treats a resource holding a property-kind const as the root", () => {
|
|
78
|
+
const code = [
|
|
79
|
+
`const red = new Row({ title: "RED" });`,
|
|
80
|
+
`export const overview = new Dashboard({ panels: [red] });`,
|
|
81
|
+
].join("\n");
|
|
82
|
+
expect(noUnusedDeclarableRule.check(context(code, PROPERTY))).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("COR004 still flags an unreferenced resource holding no property-kind declarable", () => {
|
|
86
|
+
const code = [
|
|
87
|
+
`export const logs = new Bucket({ bucketName: "logs" });`,
|
|
88
|
+
`export const overview = new Dashboard({ panels: [new Row({})] });`,
|
|
89
|
+
].join("\n");
|
|
90
|
+
expect(noUnusedDeclarableRule.check(context(code, PROPERTY)).map((d) => d.message)).toEqual([
|
|
91
|
+
expect.stringContaining("'logs'"),
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a namespace-qualified constructor resolves by its member name", () => {
|
|
96
|
+
const code = `export const d = new g.Dashboard({ panels: [new g.StatPanel({ options: { a: 1 } })] });`;
|
|
97
|
+
expect(flatDeclarationsRule.check(context(code, PROPERTY))).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintContext } from "../rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Property-kind declarables for the COR001, COR004 and COR009 heuristics
|
|
6
|
+
* (chant #2957).
|
|
7
|
+
*
|
|
8
|
+
* Those three rules were written for cloud resources, where every `new X(…)`
|
|
9
|
+
* is something the build emits. A property-kind declarable (`createProperty`)
|
|
10
|
+
* is not: it only exists inside the resource that holds it, like a Grafana
|
|
11
|
+
* panel inside its dashboard. The active lexicons name their property-kind
|
|
12
|
+
* classes through `LexiconPlugin.propertyClassNames()`, and `runLint` puts
|
|
13
|
+
* them on `LintContext.propertyClasses`. Without that set (a unit test, or a
|
|
14
|
+
* lexicon that declares none) nothing is property-kind and the rules behave
|
|
15
|
+
* as they always have.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map each locally bound import name to the name it was exported under, so
|
|
20
|
+
* `import { TimeSeriesPanel as Series }` still resolves `new Series(…)`.
|
|
21
|
+
*/
|
|
22
|
+
function importAliases(sourceFile: ts.SourceFile): Map<string, string> {
|
|
23
|
+
const aliases = new Map<string, string>();
|
|
24
|
+
for (const stmt of sourceFile.statements) {
|
|
25
|
+
if (!ts.isImportDeclaration(stmt)) continue;
|
|
26
|
+
const bindings = stmt.importClause?.namedBindings;
|
|
27
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
28
|
+
for (const el of bindings.elements) {
|
|
29
|
+
if (el.propertyName) aliases.set(el.name.text, el.propertyName.text);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return aliases;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const aliasCache = new WeakMap<ts.SourceFile, Map<string, string>>();
|
|
36
|
+
|
|
37
|
+
/** The exported class name a `new` expression constructs, if it has one. */
|
|
38
|
+
export function constructedClassName(node: ts.NewExpression, sourceFile: ts.SourceFile): string | undefined {
|
|
39
|
+
const expr = node.expression;
|
|
40
|
+
if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
|
|
41
|
+
if (!ts.isIdentifier(expr)) return undefined;
|
|
42
|
+
let aliases = aliasCache.get(sourceFile);
|
|
43
|
+
if (!aliases) {
|
|
44
|
+
aliases = importAliases(sourceFile);
|
|
45
|
+
aliasCache.set(sourceFile, aliases);
|
|
46
|
+
}
|
|
47
|
+
return aliases.get(expr.text) ?? expr.text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when `node` constructs a class an active lexicon declares property-kind. */
|
|
51
|
+
export function isPropertyKindNew(node: ts.NewExpression, context: LintContext): boolean {
|
|
52
|
+
const classes = context.propertyClasses;
|
|
53
|
+
if (!classes || classes.size === 0) return false;
|
|
54
|
+
const name = constructedClassName(node, context.sourceFile);
|
|
55
|
+
return name !== undefined && classes.has(name);
|
|
56
|
+
}
|
|
@@ -527,6 +527,28 @@ function runChant(command: string[], argv: string[], cwd: string, timeoutMs: num
|
|
|
527
527
|
});
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
/**
|
|
531
|
+
* Whether an MCP tool's document and the one its command printed are the same
|
|
532
|
+
* read (#2707). Each graph member's `cached` flag (#2876) is left out: it says
|
|
533
|
+
* whether chant's per-member cache answered that one read, not what the graph
|
|
534
|
+
* is. The tool is called first, so its read can store the cache entry the
|
|
535
|
+
* command's read is then answered from, and the flag differs while the graph
|
|
536
|
+
* does not (#3019). Everything else must be equal.
|
|
537
|
+
*/
|
|
538
|
+
export function sameReadDocument(tool: unknown, printed: unknown): boolean {
|
|
539
|
+
return isDeepStrictEqual(withoutCacheFlags(tool), withoutCacheFlags(printed));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function withoutCacheFlags(doc: unknown): unknown {
|
|
543
|
+
if (typeof doc !== "object" || doc === null || !Array.isArray((doc as { members?: unknown }).members)) return doc;
|
|
544
|
+
const members = (doc as { members: unknown[] }).members.map((m) => {
|
|
545
|
+
if (typeof m !== "object" || m === null || !("cached" in m)) return m;
|
|
546
|
+
const { cached: _cached, ...rest } = m as Record<string, unknown>;
|
|
547
|
+
return rest;
|
|
548
|
+
});
|
|
549
|
+
return { ...doc, members };
|
|
550
|
+
}
|
|
551
|
+
|
|
530
552
|
/**
|
|
531
553
|
* A transport that runs chant in the workspace `target()` names and records
|
|
532
554
|
* every call and what it printed. `reset()` clears the record before a read.
|
|
@@ -556,7 +578,7 @@ export function recordingTransport(target: () => { workspaceDir: string; chantCo
|
|
|
556
578
|
} catch {
|
|
557
579
|
printedDoc = undefined;
|
|
558
580
|
}
|
|
559
|
-
if (!
|
|
581
|
+
if (!sameReadDocument(doc, printedDoc)) problems.push(`${argv.slice(1).join(" ")}: the MCP tool ${call.name} returned a document other than chant ${argv.join(" ")} printed`);
|
|
560
582
|
return { argv: [...argv], status: 0, stdout: JSON.stringify(doc), stderr: "" };
|
|
561
583
|
};
|
|
562
584
|
const transport: ChantTransport = {
|
package/src/yaml.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { load as jsYamlLoad } from "js-yaml";
|
|
3
|
+
import { emitYAML, parseYAML, parseYAMLDocument, parseScalar, splitYAMLDocuments, YAMLParseError } from "./yaml";
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// emitYAML
|
|
@@ -538,3 +539,245 @@ describe("parseYAML — a colon without following space is not a mapping (#2013)
|
|
|
538
539
|
});
|
|
539
540
|
});
|
|
540
541
|
});
|
|
542
|
+
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// parseYAMLDocument / splitYAMLDocuments (#2965)
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
describe("parseYAMLDocument", () => {
|
|
548
|
+
test("a top-level list parses as a list", () => {
|
|
549
|
+
expect(parseYAMLDocument("- a\n- b\n")).toEqual(["a", "b"]);
|
|
550
|
+
expect(parseYAMLDocument("# rules\n\n- name: one\n expr: up\n- name: two\n")).toEqual([
|
|
551
|
+
{ name: "one", expr: "up" },
|
|
552
|
+
{ name: "two" },
|
|
553
|
+
]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("parseYAML still returns a mapping for the same input", () => {
|
|
557
|
+
expect(parseYAML("- a\n- b\n")).toEqual({});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("a mapping and JSON parse as parseYAML parses them", () => {
|
|
561
|
+
expect(parseYAMLDocument("a: 1\nb:\n - x\n")).toEqual({ a: 1, b: ["x"] });
|
|
562
|
+
expect(parseYAMLDocument("[1, 2]")).toEqual([1, 2]);
|
|
563
|
+
expect(parseYAMLDocument("")).toEqual({});
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("splitYAMLDocuments", () => {
|
|
568
|
+
test("splits on bare --- separators", () => {
|
|
569
|
+
expect(splitYAMLDocuments("a: 1\n---\nb: 2\n")).toEqual(["a: 1", "b: 2\n"]);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("a separator may carry a trailing comment", () => {
|
|
573
|
+
expect(splitYAMLDocuments("--- # first\na: 1\n--- # second\nb: 2")).toEqual(["a: 1", "b: 2"]);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("... ends a document", () => {
|
|
577
|
+
expect(splitYAMLDocuments("a: 1\n...\nb: 2\n... # done\n")).toEqual(["a: 1", "b: 2"]);
|
|
578
|
+
expect(splitYAMLDocuments("---\na: 1\n...\n---\nb: 2\n...\n")).toEqual(["a: 1", "b: 2"]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("drops empty and comment-only documents", () => {
|
|
582
|
+
expect(splitYAMLDocuments("# header\n---\n\n---\na: 1\n---\n# trailing\n")).toEqual(["a: 1"]);
|
|
583
|
+
expect(splitYAMLDocuments("")).toEqual([]);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("only a whole-line marker splits", () => {
|
|
587
|
+
expect(splitYAMLDocuments("a: ---\nb: |\n ---\n---x: 1\n")).toEqual(["a: ---\nb: |\n ---\n---x: 1\n"]);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("normalizes CRLF line endings", () => {
|
|
591
|
+
expect(splitYAMLDocuments("a: 1\r\n--- # c\r\nb: 2\r\n")).toEqual(["a: 1", "b: 2\n"]);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Blank, whitespace-only and comment lines inside a block, and other input the
|
|
596
|
+
// parser used to skip or hoist without a word (#2991). Each valid case is
|
|
597
|
+
// checked against js-yaml, so the expected value is what a full parser reads.
|
|
598
|
+
describe("parseYAML — blank lines inside blocks (#2991)", () => {
|
|
599
|
+
const same = (src: string): void => {
|
|
600
|
+
expect(parseYAMLDocument(src)).toEqual(jsYamlLoad(src));
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
test("a blank line between a key and its nested mapping (the reported bug)", () => {
|
|
604
|
+
expect(parseYAML("a:\n\n b: 1\nc: 2\n")).toEqual({ a: { b: 1 }, c: 2 });
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("a whitespace-only line between a key and its nested mapping", () => {
|
|
608
|
+
expect(parseYAML("a:\n \n b: 1\nc: 2\n")).toEqual({ a: { b: 1 }, c: 2 });
|
|
609
|
+
expect(parseYAML("a:\n\t\n b: 1\nc: 2\n")).toEqual({ a: { b: 1 }, c: 2 });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("a comment at a lower column between a key and its nested mapping", () => {
|
|
613
|
+
same("a:\n# about b\n b: 1\nc: 2\n");
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("a blank line before the first item of a block sequence", () => {
|
|
617
|
+
// This one lost every key after the sequence, not just the sequence.
|
|
618
|
+
same("a:\n\n - 1\n - 2\nc: 2\n");
|
|
619
|
+
same("a:\n\n- 1\n- 2\nc: 2\n");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("blank lines between block sequence items", () => {
|
|
623
|
+
same("a:\n - 1\n\n - \n\n - 3\nb: 2\n");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("blank lines between the keys of a nested mapping", () => {
|
|
627
|
+
same("a:\n b: 1\n\n c:\n\n d: 2\n\n e: 3\nf: 4\n");
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("blank lines before nested blocks inside a sequence item", () => {
|
|
631
|
+
same("a:\n- name: x\n env:\n\n - n: 1\n meta:\n\n k: v\n\n last: 1\nc: 2\n");
|
|
632
|
+
same("a:\n- first:\n\n k: v\n second: 2\n");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("a dash alone on its line takes the block below it", () => {
|
|
636
|
+
same("a:\n-\n x: 1\n y: 2\n-\n\n - 1\n-\n- z\n");
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("CRLF line endings, blank lines included", () => {
|
|
640
|
+
expect(parseYAML("a:\r\n\r\n b: 1\r\n c:\r\n \r\n - x\r\nd: 2\r\n")).toEqual({
|
|
641
|
+
a: { b: 1, c: ["x"] },
|
|
642
|
+
d: 2,
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("trailing whitespace on keys, items and blank lines", () => {
|
|
647
|
+
same("a: \n \n b: 1 \n c: \n\t\n d: 2\ne:\n - x \n - y: 1 \n z: 2\n");
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("a sequence item indented past `- ` keeps its keys together", () => {
|
|
651
|
+
same("- a: 1\n b: 2\n- c: 3\n");
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("the emitter's output still round-trips", () => {
|
|
655
|
+
const value = { a: { b: [{ c: 1, d: { e: "x" } }, "y"], f: null }, g: [[]] };
|
|
656
|
+
expect(parseYAML(emitYAML(value, 0).trimStart())).toEqual(value);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
describe("parseYAML — scalars spanning lines (#2991)", () => {
|
|
661
|
+
const same = (src: string): void => {
|
|
662
|
+
expect(parseYAMLDocument(src)).toEqual(jsYamlLoad(src));
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
test("a plain scalar wrapped onto more lines, as go-yaml (Helm toYaml) wraps them", () => {
|
|
666
|
+
same("description: a long line that go-yaml\n wraps at eighty columns\n\n and a paragraph\nnext: 1\n");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a wrapped plain scalar as a sequence item and as an item's key", () => {
|
|
670
|
+
same("- long text\n continued\n- name: x\n help: more\n text\n after: 1\n");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("a scalar on the line after its key", () => {
|
|
674
|
+
same("a:\n hello\n there\nb: 1\n");
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("a double-quoted scalar across lines, with a blank line and an escaped break", () => {
|
|
678
|
+
same('a: "one\n two\n\n three"\nb: "x\\\n y"\nc: 1\n');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("a single-quoted scalar across lines", () => {
|
|
682
|
+
same("a: 'it''s\n here'\nb: 1\n");
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("an unterminated quoted scalar is an error", () => {
|
|
686
|
+
expect(() => parseYAML('a: "abc\nb: 1\n')).toThrow(YAMLParseError);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe("parseYAML — quoted keys (#2991)", () => {
|
|
691
|
+
test("a quoted key holding a colon is read, not skipped", () => {
|
|
692
|
+
expect(parseYAML('"a:b": 1\nc: 2\n')).toEqual({ "a:b": 1, c: 2 });
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("quotes are not part of the key", () => {
|
|
696
|
+
expect(parseYAMLDocument("- \"a\": \"b\"\n 'c:d': 2\n")).toEqual([{ a: "b", "c:d": 2 }]);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe("parseYAML — lines it cannot place are errors, not drops (#2991)", () => {
|
|
701
|
+
const throwsAt = (src: string, line: number): void => {
|
|
702
|
+
let error: unknown;
|
|
703
|
+
try {
|
|
704
|
+
parseYAML(src);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
error = err;
|
|
707
|
+
}
|
|
708
|
+
expect(error).toBeInstanceOf(YAMLParseError);
|
|
709
|
+
expect((error as YAMLParseError).line).toBe(line);
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
test("a key indented under a scalar value", () => {
|
|
713
|
+
// Used to read as { a: 1, b: 2, c: 3 }.
|
|
714
|
+
throwsAt("a: 1\n b: 2\nc: 3\n", 2);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("a key indented past its siblings inside a sequence item", () => {
|
|
718
|
+
throwsAt("items:\n- name: x\n image: y\n stray: z\n", 4);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("a line that is neither a key nor an item", () => {
|
|
722
|
+
throwsAt("a: 1\njust text\nb: 2\n", 2);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("a dedent to a column no mapping is at", () => {
|
|
726
|
+
throwsAt(" a: 1\nb: 2\n", 2);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("a tag before a nested block names the construct", () => {
|
|
730
|
+
expect(() => parseYAML("a: !Thing\n b: 1\n")).toThrow(/the tag "!Thing"/);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("an alias with no anchor before it", () => {
|
|
734
|
+
throwsAt("a: *nowhere\n", 1);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
test("document markers are still skipped, as before", () => {
|
|
738
|
+
expect(parseYAML("---\na: 1\n...\n")).toEqual({ a: 1 });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test("a top-level list still reads as an empty mapping through parseYAML", () => {
|
|
742
|
+
expect(parseYAML("- a\n- b\n")).toEqual({});
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// GitLab CI templates and Alertmanager configs anchor a block and merge it
|
|
747
|
+
// elsewhere. Before #2991 the anchored block was hoisted into its parent and
|
|
748
|
+
// the alias read as the string "*name".
|
|
749
|
+
describe("parseYAML — anchors, aliases and merge keys (#2991)", () => {
|
|
750
|
+
const same = (src: string): void => {
|
|
751
|
+
expect(parseYAMLDocument(src)).toEqual(jsYamlLoad(src));
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
test("an anchored mapping merged into jobs, a GitLab CI template", () => {
|
|
755
|
+
same(
|
|
756
|
+
".test-template: &test-defaults\n stage: test\n before_script:\n - npm ci\n\n" +
|
|
757
|
+
"unit-test:\n <<: *test-defaults\n script:\n - npm test\n" +
|
|
758
|
+
"lint:\n stage: lint\n <<: *test-defaults\n",
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("an anchored sequence item merged into the next, an Alertmanager receiver", () => {
|
|
763
|
+
same("slack_configs:\n - &slack\n channel: '#db-alerts'\n send_resolved: true\n - <<: *slack\n channel: '#db-oncall'\n");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("anchored scalars and sequences, and aliases to them", () => {
|
|
767
|
+
same("a: &v 1\nb: *v\nc: &list\n - x\n - y\nd: *list\ne:\n - *v\n - *list\n");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("several anchors merged at once; the first listed wins", () => {
|
|
771
|
+
same("a: &a\n x: 1\n y: 1\nb: &b\n y: 2\n z: 2\nc:\n <<: [*a, *b]\n z: 3\n");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("an alias is a copy, not shared with its anchor", () => {
|
|
775
|
+
const doc = parseYAML("a: &a\n x: 1\nb: *a\n") as { a: { x: number }; b: { x: number } };
|
|
776
|
+
doc.b.x = 2;
|
|
777
|
+
expect(doc.a.x).toBe(1);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("a merge key needs a mapping", () => {
|
|
781
|
+
expect(() => parseYAML("a: &v 1\nb:\n <<: *v\n")).toThrow(YAMLParseError);
|
|
782
|
+
});
|
|
783
|
+
});
|