@cplieger/deadset-ts 8.0.0-dev.1 → 8.0.0-dev.3
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/src/analysis.js +30 -6
- package/dist/src/augmentations.js +105 -12
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/convention-rows.js +62 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +89 -0
- package/dist/src/entry-points.js +9 -18
- package/dist/src/findings/completion.js +122 -49
- package/dist/src/findings/intra-function.js +4 -2
- package/dist/src/findings/reads-and-writes.js +7 -6
- package/dist/src/findings/unused-declarations.js +13 -5
- package/dist/src/findings/visibility-narrowing.js +8 -3
- package/dist/src/graph.js +5 -3
- package/dist/src/intra-function-parts.js +45 -17
- package/dist/src/kinds.js +8 -8
- package/dist/src/manifest.js +18 -0
- package/dist/src/matrix.js +76 -4
- package/dist/src/references.js +34 -3
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +138 -11
- package/dist/src/serialization-contract.js +258 -8
- package/dist/src/suppress.js +1 -0
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/test-support.js +8 -11
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -2
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +48 -8
- package/src/augmentations.ts +134 -16
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/convention-rows.ts +72 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +96 -0
- package/src/entry-points.ts +10 -25
- package/src/file-facts.ts +3 -0
- package/src/finding.ts +5 -0
- package/src/findings/completion.ts +153 -56
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +17 -5
- package/src/findings/visibility-narrowing.ts +9 -3
- package/src/graph.ts +5 -3
- package/src/intra-function-parts.ts +55 -16
- package/src/kinds.ts +8 -8
- package/src/manifest.ts +21 -3
- package/src/matrix.ts +122 -4
- package/src/references.ts +38 -2
- package/src/report.ts +56 -16
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +162 -12
- package/src/serialization-contract.ts +309 -7
- package/src/suppress.ts +1 -0
- package/src/sweep.ts +21 -9
- package/src/test-globals.ts +275 -0
- package/src/test-support.ts +17 -13
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -2
- package/src/yaml-document.ts +377 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value of a YAML configuration document in the subset tool configurations write:
|
|
3
|
+
* block mappings and sequences, flow mappings and sequences, and plain, single-quoted and
|
|
4
|
+
* double-quoted scalars, with comments. A document using anything else, an anchor, an
|
|
5
|
+
* alias, a tag, a block scalar, a complex key or a second document, has no value here,
|
|
6
|
+
* as a JSON document that does not parse has none.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** One line of the document with its comment removed, by its indentation and its content. */
|
|
10
|
+
interface Line {
|
|
11
|
+
readonly indent: number;
|
|
12
|
+
readonly text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A failure to read the document within the subset; it ends the whole reading. */
|
|
16
|
+
class Unsupported extends Error {}
|
|
17
|
+
|
|
18
|
+
/** The characters after which a quote opens a quoted scalar rather than standing in a plain one. */
|
|
19
|
+
const SCALAR_START = new Set(["", ":", "-", "[", "{", ","]);
|
|
20
|
+
|
|
21
|
+
/** The plain scalars YAML's core schema reads as something other than a string. */
|
|
22
|
+
const CORE: ReadonlyMap<string, unknown> = new Map<string, unknown>([
|
|
23
|
+
["true", true],
|
|
24
|
+
["True", true],
|
|
25
|
+
["TRUE", true],
|
|
26
|
+
["false", false],
|
|
27
|
+
["False", false],
|
|
28
|
+
["FALSE", false],
|
|
29
|
+
["null", null],
|
|
30
|
+
["Null", null],
|
|
31
|
+
["NULL", null],
|
|
32
|
+
["~", null],
|
|
33
|
+
["", null],
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const NUMBER = /^[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/u;
|
|
37
|
+
|
|
38
|
+
/** The text of one line before its comment: a `#` at the line's start or after a blank, outside quotes. */
|
|
39
|
+
function withoutComment(line: string): string {
|
|
40
|
+
let quote: string | undefined;
|
|
41
|
+
let previous = "";
|
|
42
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
43
|
+
const char = line.charAt(at);
|
|
44
|
+
if (quote !== undefined) {
|
|
45
|
+
if (quote === '"' && char === "\\") {
|
|
46
|
+
at += 1;
|
|
47
|
+
} else if (char === quote) {
|
|
48
|
+
quote = undefined;
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if ((char === "'" || char === '"') && SCALAR_START.has(previous)) {
|
|
53
|
+
quote = char;
|
|
54
|
+
} else if (char === "#" && (at === 0 || /\s/u.test(line.charAt(at - 1)))) {
|
|
55
|
+
return line.slice(0, at);
|
|
56
|
+
}
|
|
57
|
+
if (!/\s/u.test(char)) {
|
|
58
|
+
previous = char;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return line;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The value one scalar's text denotes, quoted or plain. */
|
|
65
|
+
function scalar(text: string): unknown {
|
|
66
|
+
const value = text.trim();
|
|
67
|
+
if (value.startsWith("'")) {
|
|
68
|
+
if (!value.endsWith("'") || value.length < 2) {
|
|
69
|
+
throw new Unsupported();
|
|
70
|
+
}
|
|
71
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
72
|
+
}
|
|
73
|
+
if (value.startsWith('"')) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(value) as unknown;
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Unsupported();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (/^[&*!|>%@`?]/u.test(value) || value === "---" || value === "...") {
|
|
81
|
+
throw new Unsupported();
|
|
82
|
+
}
|
|
83
|
+
if (/:(?:\s|$)/u.test(value)) {
|
|
84
|
+
throw new Unsupported();
|
|
85
|
+
}
|
|
86
|
+
if (CORE.has(value)) {
|
|
87
|
+
return CORE.get(value);
|
|
88
|
+
}
|
|
89
|
+
return NUMBER.test(value) ? Number(value) : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reads one flow collection or flow scalar from `text` at `at`, answering the value and where it ends. */
|
|
93
|
+
function flow(text: string, start: number): { readonly value: unknown; readonly end: number } {
|
|
94
|
+
let at = start;
|
|
95
|
+
const skip = (): void => {
|
|
96
|
+
while (at < text.length && /\s/u.test(text.charAt(at))) {
|
|
97
|
+
at += 1;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
skip();
|
|
101
|
+
const open = text.charAt(at);
|
|
102
|
+
if (open === "[" || open === "{") {
|
|
103
|
+
const close = open === "[" ? "]" : "}";
|
|
104
|
+
const items: unknown[] = [];
|
|
105
|
+
const object: Record<string, unknown> = {};
|
|
106
|
+
at += 1;
|
|
107
|
+
for (;;) {
|
|
108
|
+
skip();
|
|
109
|
+
if (text.charAt(at) === close) {
|
|
110
|
+
return { value: open === "[" ? items : object, end: at + 1 };
|
|
111
|
+
}
|
|
112
|
+
const item = flow(text, at);
|
|
113
|
+
at = item.end;
|
|
114
|
+
skip();
|
|
115
|
+
if (open === "{") {
|
|
116
|
+
if (text.charAt(at) !== ":" || typeof item.value !== "string") {
|
|
117
|
+
throw new Unsupported();
|
|
118
|
+
}
|
|
119
|
+
const held = flow(text, at + 1);
|
|
120
|
+
object[item.value] = held.value;
|
|
121
|
+
at = held.end;
|
|
122
|
+
skip();
|
|
123
|
+
} else {
|
|
124
|
+
items.push(item.value);
|
|
125
|
+
}
|
|
126
|
+
if (text.charAt(at) === ",") {
|
|
127
|
+
at += 1;
|
|
128
|
+
} else if (text.charAt(at) !== close) {
|
|
129
|
+
throw new Unsupported();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (open === "'" || open === '"') {
|
|
134
|
+
let end = at + 1;
|
|
135
|
+
for (; end < text.length; end += 1) {
|
|
136
|
+
const char = text.charAt(end);
|
|
137
|
+
if (open === '"' && char === "\\") {
|
|
138
|
+
end += 1;
|
|
139
|
+
} else if (char === open && !(open === "'" && text.charAt(end + 1) === "'")) {
|
|
140
|
+
break;
|
|
141
|
+
} else if (char === open) {
|
|
142
|
+
end += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (end >= text.length) {
|
|
146
|
+
throw new Unsupported();
|
|
147
|
+
}
|
|
148
|
+
return { value: scalar(text.slice(at, end + 1)), end: end + 1 };
|
|
149
|
+
}
|
|
150
|
+
let end = at;
|
|
151
|
+
while (end < text.length && !",]}".includes(text.charAt(end))) {
|
|
152
|
+
if (text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
end += 1;
|
|
156
|
+
}
|
|
157
|
+
return { value: scalar(text.slice(at, end)), end };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A value written on one or more lines of a block, read whole: a flow node, else one plain scalar. */
|
|
161
|
+
function flowValue(text: string): unknown {
|
|
162
|
+
if (!/^["'[{]/u.test(text)) {
|
|
163
|
+
return scalar(text);
|
|
164
|
+
}
|
|
165
|
+
const read = flow(text, 0);
|
|
166
|
+
if (text.slice(read.end).trim() !== "") {
|
|
167
|
+
throw new Unsupported();
|
|
168
|
+
}
|
|
169
|
+
return read.value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The position of a mapping key's `:` in one line's content, or -1 where the content is no mapping entry. */
|
|
173
|
+
function keyEnd(text: string): number {
|
|
174
|
+
let quote: string | undefined;
|
|
175
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
176
|
+
const char = text.charAt(at);
|
|
177
|
+
if (quote !== undefined) {
|
|
178
|
+
if (quote === '"' && char === "\\") {
|
|
179
|
+
at += 1;
|
|
180
|
+
} else if (char === quote) {
|
|
181
|
+
quote = undefined;
|
|
182
|
+
}
|
|
183
|
+
} else if (at === 0 && (char === "'" || char === '"')) {
|
|
184
|
+
quote = char;
|
|
185
|
+
} else if (char === "[" || char === "{") {
|
|
186
|
+
return -1;
|
|
187
|
+
} else if (char === ":" && (at + 1 === text.length || /\s/u.test(text.charAt(at + 1)))) {
|
|
188
|
+
return at;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return -1;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** How many flow collections one text opens and leaves unclosed, quotes read through. */
|
|
195
|
+
function openCollections(text: string): number {
|
|
196
|
+
let depth = 0;
|
|
197
|
+
let quote: string | undefined;
|
|
198
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
199
|
+
const char = text.charAt(at);
|
|
200
|
+
if (quote !== undefined) {
|
|
201
|
+
if (quote === '"' && char === "\\") {
|
|
202
|
+
at += 1;
|
|
203
|
+
} else if (char === quote) {
|
|
204
|
+
quote = undefined;
|
|
205
|
+
}
|
|
206
|
+
} else if (
|
|
207
|
+
(char === "'" || char === '"') &&
|
|
208
|
+
SCALAR_START.has(text.slice(0, at).trim().slice(-1))
|
|
209
|
+
) {
|
|
210
|
+
quote = char;
|
|
211
|
+
} else if (char === "[" || char === "{") {
|
|
212
|
+
depth += 1;
|
|
213
|
+
} else if (char === "]" || char === "}") {
|
|
214
|
+
depth -= 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return depth;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Whether one line's content is a block sequence item. */
|
|
221
|
+
function isItem(text: string): boolean {
|
|
222
|
+
return text === "-" || text.startsWith("- ");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Reads the document's lines as one value. */
|
|
226
|
+
class Reader {
|
|
227
|
+
private at = 0;
|
|
228
|
+
private readonly lines: Line[];
|
|
229
|
+
|
|
230
|
+
constructor(lines: Line[]) {
|
|
231
|
+
this.lines = lines;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The whole document's value. */
|
|
235
|
+
document(): unknown {
|
|
236
|
+
const first = this.lines[0];
|
|
237
|
+
if (first === undefined) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const value = this.block(first.indent);
|
|
241
|
+
if (this.at < this.lines.length) {
|
|
242
|
+
throw new Unsupported();
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The block node whose first line is the next one, at `indent`. */
|
|
248
|
+
private block(indent: number): unknown {
|
|
249
|
+
const line = this.lines[this.at];
|
|
250
|
+
if (line === undefined) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (isItem(line.text)) {
|
|
254
|
+
return this.sequence(indent);
|
|
255
|
+
}
|
|
256
|
+
if (keyEnd(line.text) >= 0) {
|
|
257
|
+
return this.mapping(indent);
|
|
258
|
+
}
|
|
259
|
+
return this.multiline(indent);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A scalar or a flow collection whose text runs over the next lines indented deeper than `indent`. */
|
|
263
|
+
private multiline(indent: number): unknown {
|
|
264
|
+
const parts: string[] = [];
|
|
265
|
+
for (
|
|
266
|
+
let line = this.lines[this.at];
|
|
267
|
+
line !== undefined && (parts.length === 0 || line.indent > indent);
|
|
268
|
+
line = this.lines[this.at]
|
|
269
|
+
) {
|
|
270
|
+
parts.push(line.text);
|
|
271
|
+
this.at += 1;
|
|
272
|
+
}
|
|
273
|
+
return flowValue(parts.join(" "));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The value after a key or an item marker: inline text, else the deeper block below it. */
|
|
277
|
+
private valueAfter(inline: string, indent: number, sequenceAtIndent: boolean): unknown {
|
|
278
|
+
if (inline !== "") {
|
|
279
|
+
const parts = [inline];
|
|
280
|
+
// A flow collection's closing bracket may stand at the key's own column.
|
|
281
|
+
for (
|
|
282
|
+
let line = this.lines[this.at];
|
|
283
|
+
line !== undefined && (line.indent > indent || openCollections(parts.join(" ")) > 0);
|
|
284
|
+
line = this.lines[this.at]
|
|
285
|
+
) {
|
|
286
|
+
parts.push(line.text);
|
|
287
|
+
this.at += 1;
|
|
288
|
+
}
|
|
289
|
+
return flowValue(parts.join(" "));
|
|
290
|
+
}
|
|
291
|
+
const next = this.lines[this.at];
|
|
292
|
+
if (next === undefined) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text))) {
|
|
296
|
+
return this.block(next.indent);
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private mapping(indent: number): Record<string, unknown> {
|
|
302
|
+
const object: Record<string, unknown> = {};
|
|
303
|
+
for (let line = this.lines[this.at]; line?.indent === indent; line = this.lines[this.at]) {
|
|
304
|
+
const end = keyEnd(line.text);
|
|
305
|
+
if (end < 0) {
|
|
306
|
+
throw new Unsupported();
|
|
307
|
+
}
|
|
308
|
+
const key = scalar(line.text.slice(0, end));
|
|
309
|
+
if (typeof key === "object" && key !== null) {
|
|
310
|
+
throw new Unsupported();
|
|
311
|
+
}
|
|
312
|
+
this.at += 1;
|
|
313
|
+
object[String(key)] = this.valueAfter(line.text.slice(end + 1).trim(), indent, true);
|
|
314
|
+
}
|
|
315
|
+
const left = this.lines[this.at];
|
|
316
|
+
if (left !== undefined && left.indent > indent) {
|
|
317
|
+
throw new Unsupported();
|
|
318
|
+
}
|
|
319
|
+
return object;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private sequence(indent: number): unknown[] {
|
|
323
|
+
const items: unknown[] = [];
|
|
324
|
+
for (
|
|
325
|
+
let line = this.lines[this.at];
|
|
326
|
+
line?.indent === indent && isItem(line.text);
|
|
327
|
+
line = this.lines[this.at]
|
|
328
|
+
) {
|
|
329
|
+
const content = line.text.slice(1);
|
|
330
|
+
const inline = content.trimStart();
|
|
331
|
+
if (inline !== "" && (isItem(inline) || keyEnd(inline) >= 0)) {
|
|
332
|
+
// A node on the item's own line continues at its own column: read it as a block there.
|
|
333
|
+
const moved = { indent: indent + 1 + content.length - inline.length, text: inline };
|
|
334
|
+
this.lines[this.at] = moved;
|
|
335
|
+
items.push(this.block(moved.indent));
|
|
336
|
+
} else {
|
|
337
|
+
this.at += 1;
|
|
338
|
+
items.push(this.valueAfter(inline, indent, false));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const left = this.lines[this.at];
|
|
342
|
+
if (left !== undefined && left.indent > indent) {
|
|
343
|
+
throw new Unsupported();
|
|
344
|
+
}
|
|
345
|
+
return items;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** The JSON value one YAML document's text denotes, or undefined where it lies outside the subset. */
|
|
350
|
+
export function yamlDocument(text: string): unknown {
|
|
351
|
+
const lines: Line[] = [];
|
|
352
|
+
for (const raw of text.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
|
|
353
|
+
if (/^ *\t/u.test(raw)) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const content = withoutComment(raw).trimEnd();
|
|
357
|
+
const trimmed = content.trimStart();
|
|
358
|
+
if (trimmed === "") {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (trimmed === "---" && lines.length === 0) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (trimmed.startsWith("%") || trimmed === "---" || trimmed === "...") {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
lines.push({ indent: content.length - trimmed.length, text: trimmed });
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
return new Reader(lines).document();
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error instanceof Unsupported) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|