@amritk/lint 0.6.2 → 0.7.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/parsers/edit-model.js +7 -17
- package/dist/parsers/lines.d.ts +5 -0
- package/dist/parsers/lines.js +15 -3
- package/dist/parsers/yaml-locator.d.ts +56 -0
- package/dist/parsers/yaml-locator.js +122 -0
- package/dist/parsers/yaml-merge-key.d.ts +58 -0
- package/dist/parsers/yaml-merge-key.js +104 -0
- package/dist/parsers/yaml-non-finite.d.ts +22 -0
- package/dist/parsers/yaml-non-finite.js +77 -0
- package/dist/parsers/yaml.js +26 -116
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isMap, isScalar, isSeq, keyText, parseDocument } from "@amritk/yaml";
|
|
2
2
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
3
3
|
const splice = (text, start, end, replacement) => text.slice(0, start) + replacement + text.slice(end);
|
|
4
4
|
const lineStart = (text, offset) => {
|
|
@@ -20,17 +20,7 @@ const detectEol = (text) => {
|
|
|
20
20
|
const i = text.indexOf("\n");
|
|
21
21
|
return i > 0 && text.charCodeAt(i - 1) === 13 ? "\r\n" : "\n";
|
|
22
22
|
};
|
|
23
|
-
const
|
|
24
|
-
if (isScalar(key)) {
|
|
25
|
-
const v = key.value;
|
|
26
|
-
return typeof v === "string" ? v : v === null ? "null" : String(v);
|
|
27
|
-
}
|
|
28
|
-
if (isAlias(key))
|
|
29
|
-
return `*${key.source}`;
|
|
30
|
-
if (isMap(key) || isSeq(key))
|
|
31
|
-
return "";
|
|
32
|
-
return String(key);
|
|
33
|
-
};
|
|
23
|
+
const seqIndex = (segment) => typeof segment === "number" ? segment : /^(?:0|[1-9]\d*)$/.test(segment) ? Number(segment) : -1;
|
|
34
24
|
const yamlNodeAt = (root, path) => {
|
|
35
25
|
let current = root ?? void 0;
|
|
36
26
|
for (const segment of path) {
|
|
@@ -38,10 +28,10 @@ const yamlNodeAt = (root, path) => {
|
|
|
38
28
|
return void 0;
|
|
39
29
|
if (isMap(current)) {
|
|
40
30
|
const target = String(segment);
|
|
41
|
-
const pair = current.items.findLast((item) =>
|
|
31
|
+
const pair = current.items.findLast((item) => keyText(item.key) === target);
|
|
42
32
|
current = pair?.value ?? void 0;
|
|
43
33
|
} else if (isSeq(current)) {
|
|
44
|
-
current = current.items[
|
|
34
|
+
current = current.items[seqIndex(segment)];
|
|
45
35
|
} else {
|
|
46
36
|
return void 0;
|
|
47
37
|
}
|
|
@@ -53,7 +43,7 @@ const yamlPairAt = (root, path) => {
|
|
|
53
43
|
if (!parent || !isMap(parent))
|
|
54
44
|
return {};
|
|
55
45
|
const last = String(path[path.length - 1]);
|
|
56
|
-
const pair = parent.items.findLast((item) =>
|
|
46
|
+
const pair = parent.items.findLast((item) => keyText(item.key) === last);
|
|
57
47
|
if (!pair)
|
|
58
48
|
return {};
|
|
59
49
|
return { key: pair.key, ...pair.value ? { value: pair.value } : {} };
|
|
@@ -133,7 +123,7 @@ const applyYamlOp = (text, op) => {
|
|
|
133
123
|
if (!parent || !isMap(parent))
|
|
134
124
|
return text;
|
|
135
125
|
const last = String(op.path[op.path.length - 1]);
|
|
136
|
-
const index = parent.items.findLastIndex((item) =>
|
|
126
|
+
const index = parent.items.findLastIndex((item) => keyText(item.key) === last);
|
|
137
127
|
if (index === -1)
|
|
138
128
|
return text;
|
|
139
129
|
const pair = parent.items[index];
|
|
@@ -190,7 +180,7 @@ const applyYamlOp = (text, op) => {
|
|
|
190
180
|
}
|
|
191
181
|
if (!isMap(parent))
|
|
192
182
|
return text;
|
|
193
|
-
if (parent.items.some((item) =>
|
|
183
|
+
if (parent.items.some((item) => keyText(item.key) === op.key))
|
|
194
184
|
return text;
|
|
195
185
|
const pair = `${yamlInsertKey(op.key)}: ${yamlInsertValue(op.value)}`;
|
|
196
186
|
if (text.charCodeAt(parent.start) === 123) {
|
package/dist/parsers/lines.d.ts
CHANGED
|
@@ -9,5 +9,10 @@ export type LineMap = {
|
|
|
9
9
|
/**
|
|
10
10
|
* Builds a {@link LineMap} for `source`. The line-start offsets are precomputed
|
|
11
11
|
* once so each `positionAt` lookup is a binary search rather than a re-scan.
|
|
12
|
+
*
|
|
13
|
+
* A line ends at CR LF, a lone CR, or a lone LF — YAML 1.2 §5.4's
|
|
14
|
+
* `b-break ::= CR LF | CR | LF`, which is also where JSON's scanner breaks lines.
|
|
15
|
+
* Counting LF alone put every position in a CR-only file on line 0 while the
|
|
16
|
+
* parser (and `@amritk/yaml`'s `lineCounter`) put it on its real line.
|
|
12
17
|
*/
|
|
13
18
|
export declare const createLineMap: (source: string) => LineMap;
|
package/dist/parsers/lines.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
const createLineMap = (source) => {
|
|
2
2
|
const length = source.length;
|
|
3
3
|
const lineStarts = [0];
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
let nl = source.indexOf("\n");
|
|
5
|
+
let cr = source.indexOf("\r");
|
|
6
|
+
while (nl !== -1 || cr !== -1) {
|
|
7
|
+
if (cr !== -1 && (nl === -1 || cr < nl)) {
|
|
8
|
+
if (source.charCodeAt(cr + 1) === 10) {
|
|
9
|
+
lineStarts.push(cr + 2);
|
|
10
|
+
nl = source.indexOf("\n", cr + 2);
|
|
11
|
+
} else {
|
|
12
|
+
lineStarts.push(cr + 1);
|
|
13
|
+
}
|
|
14
|
+
cr = source.indexOf("\r", cr + 1);
|
|
15
|
+
} else {
|
|
16
|
+
lineStarts.push(nl + 1);
|
|
17
|
+
nl = source.indexOf("\n", nl + 1);
|
|
18
|
+
}
|
|
7
19
|
}
|
|
8
20
|
const positionAt = (offset) => {
|
|
9
21
|
const clamped = Math.max(0, Math.min(offset, length));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type YamlDocument } from '@amritk/yaml';
|
|
2
|
+
import type { LineMap } from './lines.js';
|
|
3
|
+
import type { ILocation, JsonPath } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Knobs for tests. `maxVisits` replaces {@link MAX_LOOKUP_VISITS} so the budget
|
|
6
|
+
* fallback can be reached with a small document, and `stats.visits` accumulates
|
|
7
|
+
* the work every lookup charged, so a test can bound a lookup's cost without
|
|
8
|
+
* timing it.
|
|
9
|
+
*/
|
|
10
|
+
export type YamlLocatorOptions = {
|
|
11
|
+
maxVisits?: number;
|
|
12
|
+
stats?: {
|
|
13
|
+
visits: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Builds the path-to-position lookup for a parsed YAML stream, resolving each
|
|
18
|
+
* path on demand instead of indexing every node up front.
|
|
19
|
+
*
|
|
20
|
+
* Indexing eagerly meant copying a path array, building a string key, and
|
|
21
|
+
* running two line-map searches for every node of the document — about two
|
|
22
|
+
* thirds of lint's parse time on a large spec — while a lint run only ever asks
|
|
23
|
+
* for the handful of paths that carry findings. A lookup now walks the tree from
|
|
24
|
+
* the root along the path, which is O(depth).
|
|
25
|
+
*
|
|
26
|
+
* The answers are the eager index's, including its quirks, because rulesets and
|
|
27
|
+
* fixers were written against them:
|
|
28
|
+
*
|
|
29
|
+
* - Segments match by text: `0` and `'0'` both name item 0 of a sequence and a
|
|
30
|
+
* `"0"` key of a map, the way the index's string keys did.
|
|
31
|
+
* - An alias keeps its own range, and a path beneath it descends into the
|
|
32
|
+
* anchored node.
|
|
33
|
+
* - A duplicated key resolves to the last occurrence — the index's last write.
|
|
34
|
+
* Because the index was keyed by path text, *every* occurrence of the key
|
|
35
|
+
* contributed its subtree, so a path that exists only under an earlier
|
|
36
|
+
* duplicate still resolves there. That is why a lookup tracks a list of
|
|
37
|
+
* candidate nodes per segment rather than a single node: the answer is the
|
|
38
|
+
* last candidate at the full path.
|
|
39
|
+
* - A key brought in by `<<` resolves to the node `toJS` takes it from (see
|
|
40
|
+
* {@link createMergeResolver}): explicit keys win over merged ones, an earlier
|
|
41
|
+
* merge (or an earlier item of a merge list) wins over a later one, and a
|
|
42
|
+
* merge source contributes the keys of its own projection — its explicit keys
|
|
43
|
+
* ahead of anything *it* merges. A merged key only lands on a path nothing
|
|
44
|
+
* else claimed. The eager index differed here: it walked a merge source's
|
|
45
|
+
* pairs in order, so a nested `<<` written first claimed keys the source
|
|
46
|
+
* itself overrides, and a finding landed on a value the data does not hold.
|
|
47
|
+
* - A key written with no value (`key:`) has no position of its own, so the key
|
|
48
|
+
* is absent and `closest` falls back past it.
|
|
49
|
+
* - In a multi-document stream every path starts with the document index, and
|
|
50
|
+
* the stream itself (the empty path) has no position.
|
|
51
|
+
*
|
|
52
|
+
* `closest` returns the deepest prefix that resolves, which is what popping
|
|
53
|
+
* segments off the path until the index had it found: every prefix of an indexed
|
|
54
|
+
* path was itself indexed.
|
|
55
|
+
*/
|
|
56
|
+
export declare const createYamlLocator: (docs: readonly YamlDocument[], lineMap: LineMap, options?: YamlLocatorOptions) => ((path: JsonPath, closest?: boolean) => ILocation | undefined);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { isAlias, isMap, isSeq } from "@amritk/yaml";
|
|
2
|
+
import { createMergeResolver } from "./yaml-merge-key.js";
|
|
3
|
+
const MAX_LOOKUP_VISITS = 1e5;
|
|
4
|
+
const seqIndex = (segment, length) => {
|
|
5
|
+
const index = Number(segment);
|
|
6
|
+
return Number.isInteger(index) && index >= 0 && index < length && String(index) === segment ? index : -1;
|
|
7
|
+
};
|
|
8
|
+
const identityOf = (node) => isAlias(node) ? node.target ?? node : node;
|
|
9
|
+
const dedupeCandidates = (nodes) => {
|
|
10
|
+
const last = /* @__PURE__ */ new Map();
|
|
11
|
+
for (let i = 0; i < nodes.length; i++)
|
|
12
|
+
last.set(identityOf(nodes[i]), i);
|
|
13
|
+
if (last.size === nodes.length)
|
|
14
|
+
return nodes;
|
|
15
|
+
const kept = [];
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
18
|
+
const node = nodes[i];
|
|
19
|
+
const identity = identityOf(node);
|
|
20
|
+
if (!seen.has(identity)) {
|
|
21
|
+
seen.add(identity);
|
|
22
|
+
kept.push(node);
|
|
23
|
+
} else if (last.get(identity) === i) {
|
|
24
|
+
kept.push(node);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return kept;
|
|
28
|
+
};
|
|
29
|
+
const createYamlLocator = (docs, lineMap, options = {}) => {
|
|
30
|
+
const multiDocument = docs.length > 1;
|
|
31
|
+
const maxVisits = options.maxVisits ?? MAX_LOOKUP_VISITS;
|
|
32
|
+
const budget = { left: maxVisits };
|
|
33
|
+
const merges = createMergeResolver(budget);
|
|
34
|
+
const childrenOf = (visits, segment) => {
|
|
35
|
+
const children = [];
|
|
36
|
+
for (const visit of visits) {
|
|
37
|
+
if (budget.left-- <= 0)
|
|
38
|
+
return void 0;
|
|
39
|
+
const node = isAlias(visit) ? visit.target : visit;
|
|
40
|
+
if (node == null)
|
|
41
|
+
continue;
|
|
42
|
+
if (isMap(node)) {
|
|
43
|
+
const explicit = merges.tableOf(node).explicit.get(segment);
|
|
44
|
+
if (explicit) {
|
|
45
|
+
budget.left -= explicit.length;
|
|
46
|
+
for (const value of explicit)
|
|
47
|
+
children.push(value);
|
|
48
|
+
} else if (children.length === 0) {
|
|
49
|
+
const merged = merges.mergedValueOf(node, segment);
|
|
50
|
+
if (merged)
|
|
51
|
+
children.push(merged);
|
|
52
|
+
}
|
|
53
|
+
} else if (isSeq(node)) {
|
|
54
|
+
const index = seqIndex(segment, node.items.length);
|
|
55
|
+
const item = index === -1 ? void 0 : node.items[index];
|
|
56
|
+
if (item !== void 0)
|
|
57
|
+
children.push(item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (budget.left <= 0)
|
|
61
|
+
return void 0;
|
|
62
|
+
return children.length > 1 ? dedupeCandidates(children) : children;
|
|
63
|
+
};
|
|
64
|
+
const descend = (root, path, depth, closest) => {
|
|
65
|
+
let node = root;
|
|
66
|
+
for (; depth < path.length; depth++) {
|
|
67
|
+
const target = isAlias(node) ? node.target : node;
|
|
68
|
+
const segment = String(path[depth]);
|
|
69
|
+
let child;
|
|
70
|
+
if (target != null && isMap(target))
|
|
71
|
+
child = merges.projectedValueOf(target, segment);
|
|
72
|
+
else if (target != null && isSeq(target))
|
|
73
|
+
child = target.items[seqIndex(segment, target.items.length)];
|
|
74
|
+
if (child == null)
|
|
75
|
+
return closest ? node : void 0;
|
|
76
|
+
node = child;
|
|
77
|
+
}
|
|
78
|
+
return node;
|
|
79
|
+
};
|
|
80
|
+
const locate = (path, closest) => {
|
|
81
|
+
let root;
|
|
82
|
+
let depth = 0;
|
|
83
|
+
if (multiDocument) {
|
|
84
|
+
const first = path[0];
|
|
85
|
+
if (first === void 0)
|
|
86
|
+
return void 0;
|
|
87
|
+
const index = seqIndex(String(first), docs.length);
|
|
88
|
+
root = index === -1 ? null : docs[index]?.contents ?? null;
|
|
89
|
+
depth = 1;
|
|
90
|
+
} else {
|
|
91
|
+
root = docs[0]?.contents ?? null;
|
|
92
|
+
}
|
|
93
|
+
if (root == null)
|
|
94
|
+
return void 0;
|
|
95
|
+
const start = depth;
|
|
96
|
+
let visits = [root];
|
|
97
|
+
for (; depth < path.length; depth++) {
|
|
98
|
+
const children = childrenOf(visits, String(path[depth]));
|
|
99
|
+
if (children === void 0)
|
|
100
|
+
return descend(root, path, start, closest);
|
|
101
|
+
if (children.length === 0) {
|
|
102
|
+
if (!closest)
|
|
103
|
+
return void 0;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
visits = children;
|
|
107
|
+
}
|
|
108
|
+
return visits[visits.length - 1];
|
|
109
|
+
};
|
|
110
|
+
return (path, closest = false) => {
|
|
111
|
+
budget.left = maxVisits;
|
|
112
|
+
const node = locate(path, closest);
|
|
113
|
+
if (options.stats)
|
|
114
|
+
options.stats.visits += maxVisits - budget.left;
|
|
115
|
+
if (node === void 0)
|
|
116
|
+
return void 0;
|
|
117
|
+
return { range: { start: lineMap.positionAt(node.start), end: lineMap.positionAt(node.end) } };
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
export {
|
|
121
|
+
createYamlLocator
|
|
122
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type YamlMap, type YamlNode, type YamlPair } from '@amritk/yaml';
|
|
2
|
+
/**
|
|
3
|
+
* True when a pair is a `<<` merge key, whose value folds into the parent map
|
|
4
|
+
* instead of becoming a key of its own. The parser runs with `merge: true`, so
|
|
5
|
+
* `toJS` never projects a `<<` key — which is why both the position lookup and
|
|
6
|
+
* the non-finite scan route these pairs to their merge handling rather than
|
|
7
|
+
* treating `<<` as a path segment.
|
|
8
|
+
*/
|
|
9
|
+
export declare const isMergePair: (pair: YamlPair) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* What one map holds, gathered the first time anything asks about it: the
|
|
12
|
+
* explicit pairs' values grouped by key text (in source order, so duplicates
|
|
13
|
+
* keep their order), the `<<` values in source order, and the answers already
|
|
14
|
+
* worked out for merged keys and for the full key list.
|
|
15
|
+
*/
|
|
16
|
+
export type KeyTable = {
|
|
17
|
+
explicit: Map<string, YamlNode[]>;
|
|
18
|
+
merges: (YamlNode | null)[];
|
|
19
|
+
merged: Map<string, YamlNode | null>;
|
|
20
|
+
keys: string[] | undefined;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Answers "which node does `toJS` project this key of this map from?" for maps
|
|
24
|
+
* that use `<<`, shared by the position lookup and the non-finite scan so both
|
|
25
|
+
* agree with the data.
|
|
26
|
+
*
|
|
27
|
+
* The rule is `toJsValue`/`applyMerge` in `@amritk/yaml`: a map's object is
|
|
28
|
+
* built pair by pair, an explicit key overwrites whatever is there (so the last
|
|
29
|
+
* duplicate wins, and it beats a merged key wherever it is written), and a `<<`
|
|
30
|
+
* pair copies in every key of its source's *finished* object that the map does
|
|
31
|
+
* not hold yet (so an earlier merge, or an earlier item of a merge list, wins).
|
|
32
|
+
* The source's object was itself built by the same rule first, which is the part
|
|
33
|
+
* a walk over the source's pairs in order gets wrong: in `{<<: *b, a: 1}` the
|
|
34
|
+
* nested merge is written first, but the source's own `a` is what it projects.
|
|
35
|
+
* A merge source that is not a map or a list of maps (a scalar, a `!!set`, an
|
|
36
|
+
* `!!omap`) projects to something with no own keys and contributes nothing.
|
|
37
|
+
*
|
|
38
|
+
* One quirk is kept from the eager position index this replaced: a key written
|
|
39
|
+
* with no value (`key:`) has no node to point at, so it is treated as absent —
|
|
40
|
+
* it neither answers nor shadows — rather than as the `null` `toJS` gives it.
|
|
41
|
+
*
|
|
42
|
+
* Answers are memoised per map and key. The same anchor merged into a thousand
|
|
43
|
+
* maps, or a merge chain reached by a thousand aliases, is then scanned once,
|
|
44
|
+
* not once per map per lookup. `meter.left` is decremented for every merge
|
|
45
|
+
* source examined on a cache miss so a caller with a work budget can charge the
|
|
46
|
+
* scanning to it; the scan always runs to completion, so a cached answer is
|
|
47
|
+
* never a partial one.
|
|
48
|
+
*/
|
|
49
|
+
export declare const createMergeResolver: (meter?: {
|
|
50
|
+
left: number;
|
|
51
|
+
}) => {
|
|
52
|
+
tableOf: (map: YamlMap) => KeyTable;
|
|
53
|
+
projectedValueOf: (map: YamlMap, key: string) => YamlNode | null;
|
|
54
|
+
mergedValueOf: (map: YamlMap, key: string) => YamlNode | null;
|
|
55
|
+
keysOf: (map: YamlMap) => readonly string[];
|
|
56
|
+
};
|
|
57
|
+
/** The shared merge-key resolver {@link createMergeResolver} builds. */
|
|
58
|
+
export type MergeResolver = ReturnType<typeof createMergeResolver>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isAlias, isMap, isPair, isScalar, isSeq, keyText } from "@amritk/yaml";
|
|
2
|
+
const isMergePair = (pair) => isScalar(pair.key) && pair.key.source === "<<";
|
|
3
|
+
const createMergeResolver = (meter = { left: Number.POSITIVE_INFINITY }) => {
|
|
4
|
+
const tables = /* @__PURE__ */ new Map();
|
|
5
|
+
const tableOf = (map) => {
|
|
6
|
+
const cached = tables.get(map);
|
|
7
|
+
if (cached)
|
|
8
|
+
return cached;
|
|
9
|
+
const table = { explicit: /* @__PURE__ */ new Map(), merges: [], merged: /* @__PURE__ */ new Map(), keys: void 0 };
|
|
10
|
+
for (const pair of map.items) {
|
|
11
|
+
if (!isPair(pair))
|
|
12
|
+
continue;
|
|
13
|
+
if (isMergePair(pair)) {
|
|
14
|
+
table.merges.push(pair.value);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (pair.value == null)
|
|
18
|
+
continue;
|
|
19
|
+
const key = keyText(pair.key);
|
|
20
|
+
const values = table.explicit.get(key);
|
|
21
|
+
if (values)
|
|
22
|
+
values.push(pair.value);
|
|
23
|
+
else
|
|
24
|
+
table.explicit.set(key, [pair.value]);
|
|
25
|
+
}
|
|
26
|
+
tables.set(map, table);
|
|
27
|
+
return table;
|
|
28
|
+
};
|
|
29
|
+
const eachSource = (node, visit) => {
|
|
30
|
+
meter.left--;
|
|
31
|
+
const target = node != null && isAlias(node) ? node.target : node;
|
|
32
|
+
if (target == null)
|
|
33
|
+
return null;
|
|
34
|
+
if (isSeq(target)) {
|
|
35
|
+
if (target.tag === "omap")
|
|
36
|
+
return null;
|
|
37
|
+
for (const item of target.items) {
|
|
38
|
+
const found = eachSource(item, visit);
|
|
39
|
+
if (found !== null)
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!isMap(target) || target.tag === "set")
|
|
45
|
+
return null;
|
|
46
|
+
return visit(target);
|
|
47
|
+
};
|
|
48
|
+
const mergedValueOf = (map, key) => {
|
|
49
|
+
const table = tableOf(map);
|
|
50
|
+
if (table.merges.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const cached = table.merged.get(key);
|
|
53
|
+
if (cached !== void 0)
|
|
54
|
+
return cached;
|
|
55
|
+
table.merged.set(key, null);
|
|
56
|
+
let found = null;
|
|
57
|
+
for (const merge of table.merges) {
|
|
58
|
+
found = eachSource(merge, (source) => projectedValueOf(source, key));
|
|
59
|
+
if (found !== null)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
table.merged.set(key, found);
|
|
63
|
+
return found;
|
|
64
|
+
};
|
|
65
|
+
const projectedValueOf = (map, key) => {
|
|
66
|
+
const explicit = tableOf(map).explicit.get(key);
|
|
67
|
+
if (explicit)
|
|
68
|
+
return explicit[explicit.length - 1];
|
|
69
|
+
return mergedValueOf(map, key);
|
|
70
|
+
};
|
|
71
|
+
const keysOf = (map) => {
|
|
72
|
+
const table = tableOf(map);
|
|
73
|
+
if (table.keys)
|
|
74
|
+
return table.keys;
|
|
75
|
+
const keys = [];
|
|
76
|
+
table.keys = keys;
|
|
77
|
+
const seen = /* @__PURE__ */ new Set();
|
|
78
|
+
const add = (key) => {
|
|
79
|
+
if (seen.has(key))
|
|
80
|
+
return;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
keys.push(key);
|
|
83
|
+
};
|
|
84
|
+
for (const pair of map.items) {
|
|
85
|
+
if (!isPair(pair))
|
|
86
|
+
continue;
|
|
87
|
+
if (isMergePair(pair)) {
|
|
88
|
+
eachSource(pair.value, (source) => {
|
|
89
|
+
for (const key of keysOf(source))
|
|
90
|
+
add(key);
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
} else if (pair.value != null) {
|
|
94
|
+
add(keyText(pair.key));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return keys;
|
|
98
|
+
};
|
|
99
|
+
return { tableOf, projectedValueOf, mergedValueOf, keysOf };
|
|
100
|
+
};
|
|
101
|
+
export {
|
|
102
|
+
createMergeResolver,
|
|
103
|
+
isMergePair
|
|
104
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type YamlNode, type YamlScalar } from '@amritk/yaml';
|
|
2
|
+
/**
|
|
3
|
+
* Calls `report` once for every scalar value in a document that projects to a
|
|
4
|
+
* non-finite number — `.nan`, `.inf`, `-.inf` under the core schema — in the
|
|
5
|
+
* order a depth-first walk of the data reaches them.
|
|
6
|
+
*
|
|
7
|
+
* `JSON.stringify` silently rewrites those values to `null`, which is what the
|
|
8
|
+
* `incompatibleValues` option exists to catch. This used to ride along with the
|
|
9
|
+
* eager position index; the index is on demand now, so the check gets its own
|
|
10
|
+
* walk, and only when the option is on.
|
|
11
|
+
*
|
|
12
|
+
* The walk covers what the projection can hold: map values, sequence items, the
|
|
13
|
+
* collection an alias names, and the keys a `<<` merge brings in — skipping a
|
|
14
|
+
* merged key an explicit key (or an earlier merge) already claimed, and taking
|
|
15
|
+
* each merged key from where the source's own projection takes it, since `toJS`
|
|
16
|
+
* drops every other value. Keys themselves are not checked; they project to
|
|
17
|
+
* strings. A collection is walked once however many aliases reach it and a
|
|
18
|
+
* value is reported once however many merges bring it in, so this is linear in
|
|
19
|
+
* the document even for the alias-heavy "billion laughs" shape, and a value
|
|
20
|
+
* written once is reported once.
|
|
21
|
+
*/
|
|
22
|
+
export declare const reportNonFinite: (root: YamlNode | null, report: (node: YamlScalar) => void) => void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isAlias, isMap, isPair, isScalar, isSeq, keyText } from "@amritk/yaml";
|
|
2
|
+
import { createMergeResolver, isMergePair } from "./yaml-merge-key.js";
|
|
3
|
+
const reportNonFinite = (root, report) => {
|
|
4
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5
|
+
let merges;
|
|
6
|
+
const visitMerge = (node, claimed, sources) => {
|
|
7
|
+
const target = node != null && isAlias(node) ? node.target : node;
|
|
8
|
+
if (target == null || sources.has(target))
|
|
9
|
+
return;
|
|
10
|
+
sources.add(target);
|
|
11
|
+
if (isSeq(target)) {
|
|
12
|
+
if (target.tag === "omap")
|
|
13
|
+
return;
|
|
14
|
+
for (const item of target.items)
|
|
15
|
+
visitMerge(item, claimed, sources);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (!isMap(target) || target.tag === "set")
|
|
19
|
+
return;
|
|
20
|
+
merges ??= createMergeResolver();
|
|
21
|
+
for (const key of merges.keysOf(target)) {
|
|
22
|
+
if (claimed.has(key))
|
|
23
|
+
continue;
|
|
24
|
+
claimed.add(key);
|
|
25
|
+
visit(merges.projectedValueOf(target, key));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const visit = (node) => {
|
|
29
|
+
if (node == null)
|
|
30
|
+
return;
|
|
31
|
+
if (isScalar(node)) {
|
|
32
|
+
const value = node.value;
|
|
33
|
+
if (typeof value === "number" && !Number.isFinite(value) && !seen.has(node)) {
|
|
34
|
+
seen.add(node);
|
|
35
|
+
report(node);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const target = isAlias(node) ? node.target : node;
|
|
40
|
+
if (target == null || seen.has(target))
|
|
41
|
+
return;
|
|
42
|
+
if (isSeq(target)) {
|
|
43
|
+
seen.add(target);
|
|
44
|
+
for (const item of target.items)
|
|
45
|
+
visit(item);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!isMap(target))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(target);
|
|
51
|
+
let hasMerge = false;
|
|
52
|
+
for (const pair of target.items) {
|
|
53
|
+
if (!isPair(pair))
|
|
54
|
+
continue;
|
|
55
|
+
if (isMergePair(pair))
|
|
56
|
+
hasMerge = true;
|
|
57
|
+
else
|
|
58
|
+
visit(pair.value);
|
|
59
|
+
}
|
|
60
|
+
if (!hasMerge)
|
|
61
|
+
return;
|
|
62
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
63
|
+
for (const pair of target.items) {
|
|
64
|
+
if (isPair(pair) && !isMergePair(pair) && pair.value != null)
|
|
65
|
+
claimed.add(keyText(pair.key));
|
|
66
|
+
}
|
|
67
|
+
const sources = /* @__PURE__ */ new Set();
|
|
68
|
+
for (const pair of target.items) {
|
|
69
|
+
if (isPair(pair) && isMergePair(pair))
|
|
70
|
+
visitMerge(pair.value, claimed, sources);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
visit(root);
|
|
74
|
+
};
|
|
75
|
+
export {
|
|
76
|
+
reportNonFinite
|
|
77
|
+
};
|
package/dist/parsers/yaml.js
CHANGED
|
@@ -1,42 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseAllDocuments } from "@amritk/yaml";
|
|
2
2
|
import { createLineMap } from "./lines.js";
|
|
3
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
for (const segment of path) {
|
|
7
|
-
const text = String(segment);
|
|
8
|
-
key += `${text.length}:${text}`;
|
|
9
|
-
}
|
|
10
|
-
return key;
|
|
11
|
-
};
|
|
12
|
-
const serializeComplexKey = (node) => {
|
|
13
|
-
if (isSeq(node)) {
|
|
14
|
-
return node.items.length === 0 ? "[]" : `[ ${node.items.map(flowText).join(", ")} ]`;
|
|
15
|
-
}
|
|
16
|
-
if (isMap(node)) {
|
|
17
|
-
const pairs = node.items.filter(isPair).map((pair) => `${flowText(pair.key)}: ${pair.value ? flowText(pair.value) : "null"}`);
|
|
18
|
-
return pairs.length === 0 ? "{}" : `{ ${pairs.join(", ")} }`;
|
|
19
|
-
}
|
|
20
|
-
return flowText(node);
|
|
21
|
-
};
|
|
22
|
-
const flowText = (node) => {
|
|
23
|
-
if (isScalar(node)) {
|
|
24
|
-
const v = node.value;
|
|
25
|
-
return typeof v === "string" ? v : v === null ? "null" : String(v);
|
|
26
|
-
}
|
|
27
|
-
if (isAlias(node))
|
|
28
|
-
return node.target ? flowText(node.target) : `*${node.source}`;
|
|
29
|
-
return serializeComplexKey(node);
|
|
30
|
-
};
|
|
31
|
-
const keyToString = (key) => {
|
|
32
|
-
if (isScalar(key)) {
|
|
33
|
-
const v = key.value;
|
|
34
|
-
return typeof v === "string" ? v : v === null ? "" : String(v);
|
|
35
|
-
}
|
|
36
|
-
if (isAlias(key))
|
|
37
|
-
return key.target ? keyToString(key.target) : `*${key.source}`;
|
|
38
|
-
return serializeComplexKey(key);
|
|
39
|
-
};
|
|
4
|
+
import { createYamlLocator } from "./yaml-locator.js";
|
|
5
|
+
import { reportNonFinite } from "./yaml-non-finite.js";
|
|
40
6
|
const parseYaml = (source, options = {}) => {
|
|
41
7
|
const lineMap = createLineMap(source);
|
|
42
8
|
const duplicateKeys = options.duplicateKeys;
|
|
@@ -45,7 +11,6 @@ const parseYaml = (source, options = {}) => {
|
|
|
45
11
|
const incompatibleValues = options.incompatibleValues;
|
|
46
12
|
const incompatSeverity = typeof incompatibleValues === "number" ? incompatibleValues : void 0;
|
|
47
13
|
const docs = parseAllDocuments(source, { uniqueKeys: !dedupe });
|
|
48
|
-
const index = /* @__PURE__ */ new Map();
|
|
49
14
|
const diagnostics = [];
|
|
50
15
|
const pushError = (severity, message, start, end, code) => {
|
|
51
16
|
diagnostics.push({
|
|
@@ -55,105 +20,50 @@ const parseYaml = (source, options = {}) => {
|
|
|
55
20
|
range: { start: lineMap.positionAt(start), end: lineMap.positionAt(end) }
|
|
56
21
|
});
|
|
57
22
|
};
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
end: lineMap.positionAt(node.end)
|
|
61
|
-
});
|
|
62
|
-
let budget = Math.max(1e5, source.length * 100);
|
|
63
|
-
const isMergePair = (pair) => isScalar(pair.key) && pair.key.source === "<<";
|
|
64
|
-
const walkMerge = (node, path) => {
|
|
65
|
-
const target = node != null && isAlias(node) ? node.target : node;
|
|
66
|
-
if (target == null)
|
|
67
|
-
return;
|
|
68
|
-
if (isSeq(target)) {
|
|
69
|
-
for (const item of target.items)
|
|
70
|
-
walkMerge(item, path);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (!isMap(target))
|
|
74
|
-
return;
|
|
75
|
-
for (const item of target.items) {
|
|
76
|
-
if (!isPair(item))
|
|
77
|
-
continue;
|
|
78
|
-
if (isMergePair(item)) {
|
|
79
|
-
walkMerge(item.value, path);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const childPath = [...path, keyToString(item.key)];
|
|
83
|
-
if (!index.has(pathKey(childPath)))
|
|
84
|
-
walk(item.value, childPath);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const walk = (node, path) => {
|
|
88
|
-
if (node == null || budget-- <= 0)
|
|
23
|
+
const checkIncompatible = (doc) => {
|
|
24
|
+
if (incompatSeverity === void 0)
|
|
89
25
|
return;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (incompatSeverity !== void 0 && typeof value === "number" && !Number.isFinite(value)) {
|
|
94
|
-
pushError(incompatSeverity, `Value ${String(value)} cannot be represented in JSON and will serialize to null.`, node.start, node.end, "INCOMPATIBLE_VALUE");
|
|
95
|
-
}
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const target = isAlias(node) ? node.target : node;
|
|
99
|
-
if (target == null)
|
|
100
|
-
return;
|
|
101
|
-
if (isMap(target)) {
|
|
102
|
-
const merges = [];
|
|
103
|
-
for (const item of target.items) {
|
|
104
|
-
if (!isPair(item))
|
|
105
|
-
continue;
|
|
106
|
-
if (isMergePair(item)) {
|
|
107
|
-
merges.push(item.value);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
walk(item.value, [...path, keyToString(item.key)]);
|
|
111
|
-
}
|
|
112
|
-
for (const merge of merges)
|
|
113
|
-
walkMerge(merge, path);
|
|
114
|
-
} else if (isSeq(target)) {
|
|
115
|
-
target.items.forEach((item, i) => {
|
|
116
|
-
walk(item, [...path, i]);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
26
|
+
reportNonFinite(doc.contents, (node) => {
|
|
27
|
+
pushError(incompatSeverity, `Value ${String(node.value)} cannot be represented in JSON and will serialize to null.`, node.start, node.end, "INCOMPATIBLE_VALUE");
|
|
28
|
+
});
|
|
119
29
|
};
|
|
120
30
|
const collectProblems = (doc) => {
|
|
121
31
|
for (const err of doc.errors) {
|
|
122
32
|
const severity = err.code === "DUPLICATE_KEY" ? dupSeverity : DiagnosticSeverity.Error;
|
|
123
|
-
pushError(severity, err.message, err.start, err.end);
|
|
33
|
+
pushError(severity, err.message, err.start, err.end, err.code);
|
|
124
34
|
}
|
|
125
35
|
for (const warn of doc.warnings) {
|
|
126
|
-
pushError(DiagnosticSeverity.Warning, warn.message, warn.start, warn.end);
|
|
36
|
+
pushError(DiagnosticSeverity.Warning, warn.message, warn.start, warn.end, warn.code);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const project = (doc) => {
|
|
40
|
+
try {
|
|
41
|
+
return doc.toJS();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const at = doc.contents?.start ?? 0;
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
pushError(DiagnosticSeverity.Error, message, at, at, "RESOURCE_EXHAUSTION");
|
|
46
|
+
return void 0;
|
|
127
47
|
}
|
|
128
48
|
};
|
|
129
49
|
let data;
|
|
130
50
|
if (docs.length > 1) {
|
|
131
|
-
data = docs.map((doc
|
|
132
|
-
|
|
51
|
+
data = docs.map((doc) => {
|
|
52
|
+
checkIncompatible(doc);
|
|
133
53
|
collectProblems(doc);
|
|
134
|
-
return doc
|
|
54
|
+
return project(doc);
|
|
135
55
|
});
|
|
136
56
|
} else {
|
|
137
57
|
const doc = docs[0];
|
|
138
58
|
if (doc) {
|
|
139
|
-
|
|
59
|
+
checkIncompatible(doc);
|
|
140
60
|
collectProblems(doc);
|
|
141
|
-
data = doc
|
|
61
|
+
data = project(doc);
|
|
142
62
|
} else {
|
|
143
63
|
data = null;
|
|
144
64
|
}
|
|
145
65
|
}
|
|
146
|
-
const getLocationForJsonPath = (
|
|
147
|
-
const p = path.slice();
|
|
148
|
-
while (true) {
|
|
149
|
-
const range = index.get(pathKey(p));
|
|
150
|
-
if (range)
|
|
151
|
-
return { range };
|
|
152
|
-
if (!closest || p.length === 0)
|
|
153
|
-
return void 0;
|
|
154
|
-
p.pop();
|
|
155
|
-
}
|
|
156
|
-
};
|
|
66
|
+
const getLocationForJsonPath = createYamlLocator(docs, lineMap);
|
|
157
67
|
return { data, diagnostics, getLocationForJsonPath };
|
|
158
68
|
};
|
|
159
69
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A fast, format-agnostic JSON/YAML style-guide linter with JSON Schema and custom rules.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@amritk/runtime-validators": "^0.15.1",
|
|
73
|
-
"@amritk/yaml": "^0.
|
|
73
|
+
"@amritk/yaml": "^0.8.0",
|
|
74
74
|
"jsonc-parser": "^3.3.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@amritk/resolve-refs": "^0.
|
|
77
|
+
"@amritk/resolve-refs": "^0.9.0",
|
|
78
78
|
"@stoplight/spectral-core": "^1.23.1",
|
|
79
79
|
"@stoplight/spectral-parsers": "^1.0.5",
|
|
80
80
|
"@stoplight/spectral-rulesets": "^1.22.7"
|