@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4
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/README.md +1 -1
- package/dist/src/analysis.js +61 -10
- package/dist/src/augmentations.js +63 -1
- package/dist/src/build-scripts.js +437 -0
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +56 -9
- package/dist/src/convention-rows.js +39 -2
- package/dist/src/conventions.js +15 -0
- package/dist/src/dependencies.js +98 -1
- package/dist/src/discover.js +14 -82
- package/dist/src/entry-points.js +9 -18
- package/dist/src/exempt.js +8 -0
- package/dist/src/findings/completion.js +66 -28
- 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 +4 -1
- 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 +38 -4
- package/dist/src/query.js +1 -0
- package/dist/src/report.js +23 -4
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +12 -8
- package/dist/src/roots.js +197 -42
- package/dist/src/serialization-contract.js +590 -12
- package/dist/src/sweep.js +7 -9
- package/dist/src/test-globals.js +234 -0
- package/dist/src/type-errors.js +11 -3
- package/dist/src/unseen-loads.js +107 -0
- package/dist/src/verbs/analyze.js +24 -3
- package/dist/src/workflow-steps.js +32 -15
- package/dist/src/yaml-document.js +355 -0
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +109 -12
- package/src/augmentations.ts +77 -2
- package/src/build-scripts.ts +564 -0
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +64 -12
- package/src/convention-rows.ts +47 -2
- package/src/conventions.ts +16 -0
- package/src/dependencies.ts +110 -1
- package/src/discover.ts +14 -94
- package/src/entry-points.ts +10 -25
- package/src/exempt.ts +16 -0
- package/src/file-facts.ts +3 -0
- package/src/findings/boundary.ts +2 -0
- package/src/findings/completion.ts +76 -30
- package/src/findings/intra-function.ts +4 -2
- package/src/findings/reads-and-writes.ts +7 -6
- package/src/findings/unused-declarations.ts +6 -1
- 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 +51 -4
- package/src/query.ts +11 -0
- package/src/report.ts +56 -16
- package/src/reporters/annotations.ts +21 -10
- package/src/reporters/reporter.ts +1 -3
- package/src/reporters/sarif.ts +16 -8
- package/src/roots.ts +226 -46
- package/src/serialization-contract.ts +758 -12
- package/src/sweep.ts +23 -9
- package/src/test-globals.ts +275 -0
- package/src/type-errors.ts +14 -3
- package/src/unseen-loads.ts +145 -0
- package/src/verbs/analyze.ts +28 -3
- package/src/workflow-steps.ts +35 -17
- package/src/yaml-document.ts +377 -0
|
@@ -0,0 +1,355 @@
|
|
|
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
|
+
/** A failure to read the document within the subset; it ends the whole reading. */
|
|
9
|
+
class Unsupported extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** The characters after which a quote opens a quoted scalar rather than standing in a plain one. */
|
|
12
|
+
const SCALAR_START = new Set(["", ":", "-", "[", "{", ","]);
|
|
13
|
+
/** The plain scalars YAML's core schema reads as something other than a string. */
|
|
14
|
+
const CORE = new Map([
|
|
15
|
+
["true", true],
|
|
16
|
+
["True", true],
|
|
17
|
+
["TRUE", true],
|
|
18
|
+
["false", false],
|
|
19
|
+
["False", false],
|
|
20
|
+
["FALSE", false],
|
|
21
|
+
["null", null],
|
|
22
|
+
["Null", null],
|
|
23
|
+
["NULL", null],
|
|
24
|
+
["~", null],
|
|
25
|
+
["", null],
|
|
26
|
+
]);
|
|
27
|
+
const NUMBER = /^[-+]?(?:\d+|\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/u;
|
|
28
|
+
/** The text of one line before its comment: a `#` at the line's start or after a blank, outside quotes. */
|
|
29
|
+
function withoutComment(line) {
|
|
30
|
+
let quote;
|
|
31
|
+
let previous = "";
|
|
32
|
+
for (let at = 0; at < line.length; at += 1) {
|
|
33
|
+
const char = line.charAt(at);
|
|
34
|
+
if (quote !== undefined) {
|
|
35
|
+
if (quote === '"' && char === "\\") {
|
|
36
|
+
at += 1;
|
|
37
|
+
}
|
|
38
|
+
else if (char === quote) {
|
|
39
|
+
quote = undefined;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if ((char === "'" || char === '"') && SCALAR_START.has(previous)) {
|
|
44
|
+
quote = char;
|
|
45
|
+
}
|
|
46
|
+
else if (char === "#" && (at === 0 || /\s/u.test(line.charAt(at - 1)))) {
|
|
47
|
+
return line.slice(0, at);
|
|
48
|
+
}
|
|
49
|
+
if (!/\s/u.test(char)) {
|
|
50
|
+
previous = char;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return line;
|
|
54
|
+
}
|
|
55
|
+
/** The value one scalar's text denotes, quoted or plain. */
|
|
56
|
+
function scalar(text) {
|
|
57
|
+
const value = text.trim();
|
|
58
|
+
if (value.startsWith("'")) {
|
|
59
|
+
if (!value.endsWith("'") || value.length < 2) {
|
|
60
|
+
throw new Unsupported();
|
|
61
|
+
}
|
|
62
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
63
|
+
}
|
|
64
|
+
if (value.startsWith('"')) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(value);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Unsupported();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (/^[&*!|>%@`?]/u.test(value) || value === "---" || value === "...") {
|
|
73
|
+
throw new Unsupported();
|
|
74
|
+
}
|
|
75
|
+
if (/:(?:\s|$)/u.test(value)) {
|
|
76
|
+
throw new Unsupported();
|
|
77
|
+
}
|
|
78
|
+
if (CORE.has(value)) {
|
|
79
|
+
return CORE.get(value);
|
|
80
|
+
}
|
|
81
|
+
return NUMBER.test(value) ? Number(value) : value;
|
|
82
|
+
}
|
|
83
|
+
/** Reads one flow collection or flow scalar from `text` at `at`, answering the value and where it ends. */
|
|
84
|
+
function flow(text, start) {
|
|
85
|
+
let at = start;
|
|
86
|
+
const skip = () => {
|
|
87
|
+
while (at < text.length && /\s/u.test(text.charAt(at))) {
|
|
88
|
+
at += 1;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
skip();
|
|
92
|
+
const open = text.charAt(at);
|
|
93
|
+
if (open === "[" || open === "{") {
|
|
94
|
+
const close = open === "[" ? "]" : "}";
|
|
95
|
+
const items = [];
|
|
96
|
+
const object = {};
|
|
97
|
+
at += 1;
|
|
98
|
+
for (;;) {
|
|
99
|
+
skip();
|
|
100
|
+
if (text.charAt(at) === close) {
|
|
101
|
+
return { value: open === "[" ? items : object, end: at + 1 };
|
|
102
|
+
}
|
|
103
|
+
const item = flow(text, at);
|
|
104
|
+
at = item.end;
|
|
105
|
+
skip();
|
|
106
|
+
if (open === "{") {
|
|
107
|
+
if (text.charAt(at) !== ":" || typeof item.value !== "string") {
|
|
108
|
+
throw new Unsupported();
|
|
109
|
+
}
|
|
110
|
+
const held = flow(text, at + 1);
|
|
111
|
+
object[item.value] = held.value;
|
|
112
|
+
at = held.end;
|
|
113
|
+
skip();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
items.push(item.value);
|
|
117
|
+
}
|
|
118
|
+
if (text.charAt(at) === ",") {
|
|
119
|
+
at += 1;
|
|
120
|
+
}
|
|
121
|
+
else if (text.charAt(at) !== close) {
|
|
122
|
+
throw new Unsupported();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (open === "'" || open === '"') {
|
|
127
|
+
let end = at + 1;
|
|
128
|
+
for (; end < text.length; end += 1) {
|
|
129
|
+
const char = text.charAt(end);
|
|
130
|
+
if (open === '"' && char === "\\") {
|
|
131
|
+
end += 1;
|
|
132
|
+
}
|
|
133
|
+
else if (char === open && !(open === "'" && text.charAt(end + 1) === "'")) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
else if (char === open) {
|
|
137
|
+
end += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (end >= text.length) {
|
|
141
|
+
throw new Unsupported();
|
|
142
|
+
}
|
|
143
|
+
return { value: scalar(text.slice(at, end + 1)), end: end + 1 };
|
|
144
|
+
}
|
|
145
|
+
let end = at;
|
|
146
|
+
while (end < text.length && !",]}".includes(text.charAt(end))) {
|
|
147
|
+
if (text.charAt(end) === ":" && /[\s,\]}]/u.test(text.charAt(end + 1) || " ")) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
end += 1;
|
|
151
|
+
}
|
|
152
|
+
return { value: scalar(text.slice(at, end)), end };
|
|
153
|
+
}
|
|
154
|
+
/** A value written on one or more lines of a block, read whole: a flow node, else one plain scalar. */
|
|
155
|
+
function flowValue(text) {
|
|
156
|
+
if (!/^["'[{]/u.test(text)) {
|
|
157
|
+
return scalar(text);
|
|
158
|
+
}
|
|
159
|
+
const read = flow(text, 0);
|
|
160
|
+
if (text.slice(read.end).trim() !== "") {
|
|
161
|
+
throw new Unsupported();
|
|
162
|
+
}
|
|
163
|
+
return read.value;
|
|
164
|
+
}
|
|
165
|
+
/** The position of a mapping key's `:` in one line's content, or -1 where the content is no mapping entry. */
|
|
166
|
+
function keyEnd(text) {
|
|
167
|
+
let quote;
|
|
168
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
169
|
+
const char = text.charAt(at);
|
|
170
|
+
if (quote !== undefined) {
|
|
171
|
+
if (quote === '"' && char === "\\") {
|
|
172
|
+
at += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (char === quote) {
|
|
175
|
+
quote = undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (at === 0 && (char === "'" || char === '"')) {
|
|
179
|
+
quote = char;
|
|
180
|
+
}
|
|
181
|
+
else if (char === "[" || char === "{") {
|
|
182
|
+
return -1;
|
|
183
|
+
}
|
|
184
|
+
else if (char === ":" && (at + 1 === text.length || /\s/u.test(text.charAt(at + 1)))) {
|
|
185
|
+
return at;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return -1;
|
|
189
|
+
}
|
|
190
|
+
/** How many flow collections one text opens and leaves unclosed, quotes read through. */
|
|
191
|
+
function openCollections(text) {
|
|
192
|
+
let depth = 0;
|
|
193
|
+
let quote;
|
|
194
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
195
|
+
const char = text.charAt(at);
|
|
196
|
+
if (quote !== undefined) {
|
|
197
|
+
if (quote === '"' && char === "\\") {
|
|
198
|
+
at += 1;
|
|
199
|
+
}
|
|
200
|
+
else if (char === quote) {
|
|
201
|
+
quote = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if ((char === "'" || char === '"') &&
|
|
205
|
+
SCALAR_START.has(text.slice(0, at).trim().slice(-1))) {
|
|
206
|
+
quote = char;
|
|
207
|
+
}
|
|
208
|
+
else if (char === "[" || char === "{") {
|
|
209
|
+
depth += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (char === "]" || char === "}") {
|
|
212
|
+
depth -= 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return depth;
|
|
216
|
+
}
|
|
217
|
+
/** Whether one line's content is a block sequence item. */
|
|
218
|
+
function isItem(text) {
|
|
219
|
+
return text === "-" || text.startsWith("- ");
|
|
220
|
+
}
|
|
221
|
+
/** Reads the document's lines as one value. */
|
|
222
|
+
class Reader {
|
|
223
|
+
at = 0;
|
|
224
|
+
lines;
|
|
225
|
+
constructor(lines) {
|
|
226
|
+
this.lines = lines;
|
|
227
|
+
}
|
|
228
|
+
/** The whole document's value. */
|
|
229
|
+
document() {
|
|
230
|
+
const first = this.lines[0];
|
|
231
|
+
if (first === undefined) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const value = this.block(first.indent);
|
|
235
|
+
if (this.at < this.lines.length) {
|
|
236
|
+
throw new Unsupported();
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
/** The block node whose first line is the next one, at `indent`. */
|
|
241
|
+
block(indent) {
|
|
242
|
+
const line = this.lines[this.at];
|
|
243
|
+
if (line === undefined) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (isItem(line.text)) {
|
|
247
|
+
return this.sequence(indent);
|
|
248
|
+
}
|
|
249
|
+
if (keyEnd(line.text) >= 0) {
|
|
250
|
+
return this.mapping(indent);
|
|
251
|
+
}
|
|
252
|
+
return this.multiline(indent);
|
|
253
|
+
}
|
|
254
|
+
/** A scalar or a flow collection whose text runs over the next lines indented deeper than `indent`. */
|
|
255
|
+
multiline(indent) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
for (let line = this.lines[this.at]; line !== undefined && (parts.length === 0 || line.indent > indent); line = this.lines[this.at]) {
|
|
258
|
+
parts.push(line.text);
|
|
259
|
+
this.at += 1;
|
|
260
|
+
}
|
|
261
|
+
return flowValue(parts.join(" "));
|
|
262
|
+
}
|
|
263
|
+
/** The value after a key or an item marker: inline text, else the deeper block below it. */
|
|
264
|
+
valueAfter(inline, indent, sequenceAtIndent) {
|
|
265
|
+
if (inline !== "") {
|
|
266
|
+
const parts = [inline];
|
|
267
|
+
// A flow collection's closing bracket may stand at the key's own column.
|
|
268
|
+
for (let line = this.lines[this.at]; line !== undefined && (line.indent > indent || openCollections(parts.join(" ")) > 0); line = this.lines[this.at]) {
|
|
269
|
+
parts.push(line.text);
|
|
270
|
+
this.at += 1;
|
|
271
|
+
}
|
|
272
|
+
return flowValue(parts.join(" "));
|
|
273
|
+
}
|
|
274
|
+
const next = this.lines[this.at];
|
|
275
|
+
if (next === undefined) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (next.indent > indent || (sequenceAtIndent && next.indent === indent && isItem(next.text))) {
|
|
279
|
+
return this.block(next.indent);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
mapping(indent) {
|
|
284
|
+
const object = {};
|
|
285
|
+
for (let line = this.lines[this.at]; line?.indent === indent; line = this.lines[this.at]) {
|
|
286
|
+
const end = keyEnd(line.text);
|
|
287
|
+
if (end < 0) {
|
|
288
|
+
throw new Unsupported();
|
|
289
|
+
}
|
|
290
|
+
const key = scalar(line.text.slice(0, end));
|
|
291
|
+
if (typeof key === "object" && key !== null) {
|
|
292
|
+
throw new Unsupported();
|
|
293
|
+
}
|
|
294
|
+
this.at += 1;
|
|
295
|
+
object[String(key)] = this.valueAfter(line.text.slice(end + 1).trim(), indent, true);
|
|
296
|
+
}
|
|
297
|
+
const left = this.lines[this.at];
|
|
298
|
+
if (left !== undefined && left.indent > indent) {
|
|
299
|
+
throw new Unsupported();
|
|
300
|
+
}
|
|
301
|
+
return object;
|
|
302
|
+
}
|
|
303
|
+
sequence(indent) {
|
|
304
|
+
const items = [];
|
|
305
|
+
for (let line = this.lines[this.at]; line?.indent === indent && isItem(line.text); line = this.lines[this.at]) {
|
|
306
|
+
const content = line.text.slice(1);
|
|
307
|
+
const inline = content.trimStart();
|
|
308
|
+
if (inline !== "" && (isItem(inline) || keyEnd(inline) >= 0)) {
|
|
309
|
+
// A node on the item's own line continues at its own column: read it as a block there.
|
|
310
|
+
const moved = { indent: indent + 1 + content.length - inline.length, text: inline };
|
|
311
|
+
this.lines[this.at] = moved;
|
|
312
|
+
items.push(this.block(moved.indent));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
this.at += 1;
|
|
316
|
+
items.push(this.valueAfter(inline, indent, false));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const left = this.lines[this.at];
|
|
320
|
+
if (left !== undefined && left.indent > indent) {
|
|
321
|
+
throw new Unsupported();
|
|
322
|
+
}
|
|
323
|
+
return items;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** The JSON value one YAML document's text denotes, or undefined where it lies outside the subset. */
|
|
327
|
+
export function yamlDocument(text) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
for (const raw of text.replace(/^\uFEFF/u, "").split(/\r?\n/u)) {
|
|
330
|
+
if (/^ *\t/u.test(raw)) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const content = withoutComment(raw).trimEnd();
|
|
334
|
+
const trimmed = content.trimStart();
|
|
335
|
+
if (trimmed === "") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (trimmed === "---" && lines.length === 0) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (trimmed.startsWith("%") || trimmed === "---" || trimmed === "...") {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
lines.push({ indent: content.length - trimmed.length, text: trimmed });
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return new Reader(lines).document();
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof Unsupported) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/deadset-ts",
|
|
3
|
-
"version": "8.0.0-dev.
|
|
3
|
+
"version": "8.0.0-dev.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Deterministic dead-code analysis for TypeScript, class and type members included",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lint:fix": "eslint . --fix && prettier --write ."
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@typescript/native": "npm:typescript@7.1.0-dev.
|
|
33
|
+
"@typescript/native": "npm:typescript@7.1.0-dev.20261014.1"
|
|
34
34
|
},
|
|
35
35
|
"overrides": {
|
|
36
36
|
"qs": "^6.15.2"
|
package/src/analysis.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* another project's checker, which holds only for views the session hands out.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { isStringLiteralLikeNode } from "@typescript/native/ast";
|
|
9
|
+
import { isStringLiteralLikeNode, type SourceFile } from "@typescript/native/ast";
|
|
10
10
|
import { lineOf, readComponent } from "./component-files.ts";
|
|
11
11
|
import { kindOf, type Config, type Provenance } from "./config.ts";
|
|
12
12
|
import {
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
holdingWhile,
|
|
57
57
|
isUnansweredRecord,
|
|
58
58
|
retainedIn,
|
|
59
|
+
writtenRecords,
|
|
59
60
|
type Detector,
|
|
60
61
|
type Detectors,
|
|
61
62
|
} from "./exempt.ts";
|
|
@@ -107,14 +108,18 @@ import { references, testFileRulesOf, type Reference, type TestFileRule } from "
|
|
|
107
108
|
import {
|
|
108
109
|
roots,
|
|
109
110
|
unmatchedEverywhere,
|
|
111
|
+
type Root,
|
|
110
112
|
type RootKind,
|
|
111
113
|
type RootOptions,
|
|
112
114
|
type Roots,
|
|
113
115
|
} from "./roots.ts";
|
|
114
116
|
import type { Scope } from "./scope.ts";
|
|
115
|
-
import { serializationContract } from "./serialization-contract.ts";
|
|
117
|
+
import { decodedMembers, serializationContract, type Decoded } from "./serialization-contract.ts";
|
|
116
118
|
import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
|
|
119
|
+
import { mergedGlobalInterfaces } from "./augmentations.ts";
|
|
120
|
+
import { unseenLoadsOf, type UnseenLoad } from "./unseen-loads.ts";
|
|
117
121
|
import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
|
|
122
|
+
import { GLOBALS_RUNNER, namesTestGlobals, withTestGlobals } from "./test-globals.ts";
|
|
118
123
|
import { supportReferences, testSupportFiles, type TestEvidence } from "./test-support.ts";
|
|
119
124
|
import {
|
|
120
125
|
diagnosticsOf,
|
|
@@ -428,11 +433,17 @@ function readProjects<Answer>(
|
|
|
428
433
|
const joins = discovered.derived
|
|
429
434
|
? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
|
|
430
435
|
: new Map<string, readonly string[]>();
|
|
431
|
-
const programs =
|
|
436
|
+
const programs = withTestGlobals(
|
|
432
437
|
engine,
|
|
433
438
|
host,
|
|
434
|
-
|
|
435
|
-
|
|
439
|
+
parse,
|
|
440
|
+
discovered.configFiles,
|
|
441
|
+
withBaseUrlPaths(
|
|
442
|
+
engine,
|
|
443
|
+
host,
|
|
444
|
+
[...discovered.configFiles, ...consumerOf.keys()],
|
|
445
|
+
workspace?.programs ?? new Map(),
|
|
446
|
+
),
|
|
436
447
|
);
|
|
437
448
|
const session = runSession(
|
|
438
449
|
engine,
|
|
@@ -784,6 +795,8 @@ interface SweptProject<Extra> {
|
|
|
784
795
|
readonly exempt: readonly Exemption[];
|
|
785
796
|
/** The declarations of the project an unanswered question could have kept live. */
|
|
786
797
|
readonly unanswered: readonly string[];
|
|
798
|
+
/** The data members a generic declared outside the analysis fills, which it may read. */
|
|
799
|
+
readonly filledOutside: readonly string[];
|
|
787
800
|
readonly typeErrorSkips: readonly TypeErrorSkip[];
|
|
788
801
|
readonly skipped: readonly SkippedUnit[];
|
|
789
802
|
/** The component files the project holds of its own, by path below the target root. */
|
|
@@ -804,6 +817,10 @@ interface ReadProject<Extra> {
|
|
|
804
817
|
readonly componentWarnings: readonly ComponentWarning[];
|
|
805
818
|
/** Per own file below the target root, the module specifiers its imports write. */
|
|
806
819
|
readonly imports: ReadonlyMap<string, readonly string[]>;
|
|
820
|
+
/** The global augmentations a declaration outside the project names. */
|
|
821
|
+
readonly augmented: readonly Root[];
|
|
822
|
+
/** The data members its production code decodes into. */
|
|
823
|
+
readonly decoded: Decoded;
|
|
807
824
|
readonly extra: Extra;
|
|
808
825
|
}
|
|
809
826
|
|
|
@@ -940,6 +957,9 @@ function readRun<Extra>(
|
|
|
940
957
|
disabled,
|
|
941
958
|
);
|
|
942
959
|
const components = project.ownSourceFiles().filter(isComponentFile);
|
|
960
|
+
const injected = project.program
|
|
961
|
+
.getSourceFileNames()
|
|
962
|
+
.some((name) => name.endsWith(`/node_modules/${GLOBALS_RUNNER}/globals.d.ts`));
|
|
943
963
|
const pathOf = (file: (typeof components)[number]): string =>
|
|
944
964
|
relativePath(targetRoot, file.fileName) ?? file.fileName;
|
|
945
965
|
return {
|
|
@@ -948,12 +968,27 @@ function readRun<Extra>(
|
|
|
948
968
|
testFiles: resolved.testFilePaths,
|
|
949
969
|
evidence,
|
|
950
970
|
componentFiles: components.map(pathOf),
|
|
971
|
+
augmented: mergedGlobalInterfaces(
|
|
972
|
+
project,
|
|
973
|
+
projectRead.held,
|
|
974
|
+
sourceFilesOf(project, targetRoot),
|
|
975
|
+
).map((id): Root => ({ id, kind: "global-augmentation", source: "" })),
|
|
951
976
|
imports: new Map(
|
|
952
977
|
project.ownSourceFiles().flatMap((file) => {
|
|
953
978
|
const path = relativePath(targetRoot, file.fileName);
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
979
|
+
if (path === undefined) {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
const specifiers = file.imports.filter(isStringLiteralLikeNode).map((one) => one.text);
|
|
983
|
+
// A file calling the globals a program declares is written for their runner.
|
|
984
|
+
return [
|
|
985
|
+
[
|
|
986
|
+
path,
|
|
987
|
+
injected && namesTestGlobals(project, file)
|
|
988
|
+
? [...specifiers, GLOBALS_RUNNER]
|
|
989
|
+
: specifiers,
|
|
990
|
+
],
|
|
991
|
+
];
|
|
957
992
|
}),
|
|
958
993
|
),
|
|
959
994
|
componentWarnings: components.flatMap((file) =>
|
|
@@ -963,6 +998,12 @@ function readRun<Extra>(
|
|
|
963
998
|
reason: warning.reason,
|
|
964
999
|
})),
|
|
965
1000
|
),
|
|
1001
|
+
decoded: decodedMembers(
|
|
1002
|
+
project,
|
|
1003
|
+
projectRead.held,
|
|
1004
|
+
productionFiles(project, targetRoot, resolved.testFilePaths),
|
|
1005
|
+
consumers,
|
|
1006
|
+
),
|
|
966
1007
|
extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
|
|
967
1008
|
};
|
|
968
1009
|
});
|
|
@@ -984,14 +1025,31 @@ function readRun<Extra>(
|
|
|
984
1025
|
withOutside.flatMap((one) => one.testFiles),
|
|
985
1026
|
testEvidence(host, targetRoot, withOutside),
|
|
986
1027
|
);
|
|
1028
|
+
// A member a decode fills or a copy carries is written: a value decoded and sent again carries it on.
|
|
1029
|
+
const written = new Set([
|
|
1030
|
+
...countedWrites(
|
|
1031
|
+
[...withOutside.flatMap((one) => one.references), ...read.consumers.references],
|
|
1032
|
+
support,
|
|
1033
|
+
mode,
|
|
1034
|
+
),
|
|
1035
|
+
...withOutside.flatMap((one) => [
|
|
1036
|
+
...one.decoded.filled,
|
|
1037
|
+
...one.decoded.parsed,
|
|
1038
|
+
...one.decoded.copied,
|
|
1039
|
+
]),
|
|
1040
|
+
]);
|
|
1041
|
+
const evidence = withOutside.map((one) => writtenRecords(one.evidence, written));
|
|
987
1042
|
const settled = settledSatisfaction(
|
|
988
|
-
withOutside.map((one) => ({
|
|
1043
|
+
withOutside.map((one, index) => ({
|
|
1044
|
+
evidence: evidence[index] ?? [],
|
|
1045
|
+
held: one.projectRead.held,
|
|
1046
|
+
})),
|
|
989
1047
|
[...withOutside.flatMap((one) => one.references), ...read.consumers.references],
|
|
990
1048
|
);
|
|
991
1049
|
const answers = withOutside.map((one, index): SweptProject<Extra> => {
|
|
992
1050
|
const { projectRead } = one;
|
|
993
1051
|
const satisfied = settled[index];
|
|
994
|
-
const detected = heldRecords(satisfied?.evidence ??
|
|
1052
|
+
const detected = heldRecords(satisfied?.evidence ?? evidence[index] ?? [], {
|
|
995
1053
|
mode,
|
|
996
1054
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
997
1055
|
});
|
|
@@ -1004,7 +1062,9 @@ function readRun<Extra>(
|
|
|
1004
1062
|
configuration: projectRead.configuration,
|
|
1005
1063
|
symbols: projectRead.held.symbols,
|
|
1006
1064
|
references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
|
|
1007
|
-
|
|
1065
|
+
// The augmentations are rooted after test support is read: an outside declaration
|
|
1066
|
+
// naming one calls nothing of its file.
|
|
1067
|
+
roots: [...projectRead.rooted.liveUnderReachability, ...one.augmented],
|
|
1008
1068
|
testFiles: [...one.testFiles, ...supportFiles],
|
|
1009
1069
|
...(supportFiles.length === 0 ? {} : { supportFiles }),
|
|
1010
1070
|
...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
|
|
@@ -1027,7 +1087,9 @@ function readRun<Extra>(
|
|
|
1027
1087
|
.filter((reference) => reference.resolution === "by-name")
|
|
1028
1088
|
.map((reference) => reference.to),
|
|
1029
1089
|
...exempt.filter(isUnansweredRecord).map((record) => record.id),
|
|
1090
|
+
...one.decoded.unanswered,
|
|
1030
1091
|
],
|
|
1092
|
+
filledOutside: one.decoded.filled,
|
|
1031
1093
|
typeErrorSkips: projectRead.typeErrorSkips,
|
|
1032
1094
|
unmappedEntries: projectRead.rooted.unmappedEntries,
|
|
1033
1095
|
skipped: projectRead.skipped,
|
|
@@ -1130,6 +1192,22 @@ function evidenceReferences(
|
|
|
1130
1192
|
});
|
|
1131
1193
|
}
|
|
1132
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* The declarations a write the mode counts stores into, a write a test-support file
|
|
1197
|
+
* makes being a test's.
|
|
1198
|
+
*/
|
|
1199
|
+
function countedWrites(
|
|
1200
|
+
references: readonly Reference[],
|
|
1201
|
+
support: ReadonlySet<string>,
|
|
1202
|
+
mode: Mode,
|
|
1203
|
+
): ReadonlySet<string> {
|
|
1204
|
+
return new Set(
|
|
1205
|
+
supportReferences(references, support).flatMap((reference) =>
|
|
1206
|
+
reference.use === "write" && !(mode.production && reference.test) ? [reference.to] : [],
|
|
1207
|
+
),
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1133
1211
|
/**
|
|
1134
1212
|
* The declarations every reference the mode counts stores into, a loaded consumer's
|
|
1135
1213
|
* reference counting as a read: what the write-only kind would report but for a record.
|
|
@@ -1246,6 +1324,19 @@ interface EmitterExtra {
|
|
|
1246
1324
|
readonly directives: readonly InlineDirective[];
|
|
1247
1325
|
readonly declarationsNamed: readonly boolean[];
|
|
1248
1326
|
readonly parts: ProjectParts;
|
|
1327
|
+
readonly unseenLoads: readonly UnseenLoad[];
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** The project's own files below the target root that are no test file. */
|
|
1331
|
+
function productionFiles<Brand>(
|
|
1332
|
+
project: ProjectView<Brand>,
|
|
1333
|
+
targetRoot: string,
|
|
1334
|
+
testFiles: readonly string[],
|
|
1335
|
+
): readonly SourceFile[] {
|
|
1336
|
+
const tests = new Set(testFiles);
|
|
1337
|
+
return [...sourceFilesOf(project, targetRoot).byPath].flatMap(([path, file]) =>
|
|
1338
|
+
tests.has(path) ? [] : [file],
|
|
1339
|
+
);
|
|
1249
1340
|
}
|
|
1250
1341
|
|
|
1251
1342
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
@@ -1281,9 +1372,10 @@ export function readEmitterRun(
|
|
|
1281
1372
|
),
|
|
1282
1373
|
declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
|
|
1283
1374
|
parts: projectParts(project, read.held, references, targetRoot),
|
|
1375
|
+
unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
|
|
1284
1376
|
}),
|
|
1285
1377
|
);
|
|
1286
|
-
// Whether a declaration carries the marker decides its code, so one whose
|
|
1378
|
+
// Whether a declaration carries the marker decides its code, so one whose answer went
|
|
1287
1379
|
// unread is reported by nothing.
|
|
1288
1380
|
const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
|
|
1289
1381
|
return unread.length === 0
|
|
@@ -1386,6 +1478,7 @@ export function emitterInputOver(
|
|
|
1386
1478
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
1387
1479
|
generated: new Set(projects.flatMap((one) => one.extra.generated)),
|
|
1388
1480
|
inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
|
|
1481
|
+
unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
|
|
1389
1482
|
},
|
|
1390
1483
|
boundary: {
|
|
1391
1484
|
consumers: {
|
|
@@ -1394,6 +1487,7 @@ export function emitterInputOver(
|
|
|
1394
1487
|
},
|
|
1395
1488
|
encapsulated: readManifest(host, targetRoot).declaresExports,
|
|
1396
1489
|
edges: readEdgeSides(host, targetRoot),
|
|
1490
|
+
filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
|
|
1397
1491
|
},
|
|
1398
1492
|
selfCheck,
|
|
1399
1493
|
intraFunction: intraFunctionFacts(
|
|
@@ -1476,6 +1570,7 @@ function readAnalysis(
|
|
|
1476
1570
|
librariesUnfound: read.librariesUnfound,
|
|
1477
1571
|
conventionsApplied: read.conventionsApplied,
|
|
1478
1572
|
unmappedEntries: unmappedEverywhere(read.projects),
|
|
1573
|
+
unseenLoads: read.projects.flatMap((one) => one.extra.unseenLoads),
|
|
1479
1574
|
},
|
|
1480
1575
|
configured: read.projects.map((one) => one.configured),
|
|
1481
1576
|
symbols,
|
|
@@ -1574,6 +1669,8 @@ interface RunFacts {
|
|
|
1574
1669
|
readonly conventionsApplied: readonly AppliedConvention[];
|
|
1575
1670
|
/** The manifest entries no project of the run reads back to a file, each once. */
|
|
1576
1671
|
readonly unmappedEntries: readonly ManifestEntry[];
|
|
1672
|
+
/** The loads of production files whose target the run cannot resolve, per configuration. */
|
|
1673
|
+
readonly unseenLoads: readonly UnseenLoad[];
|
|
1577
1674
|
}
|
|
1578
1675
|
|
|
1579
1676
|
/** One configuration whose component files a run cannot read. */
|