@cplieger/deadset-ts 7.0.0-dev.1 → 7.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 +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -50,10 +50,16 @@ export const RESOLVED_EXTENSIONS = [
|
|
|
50
50
|
".mjs",
|
|
51
51
|
".cjs",
|
|
52
52
|
];
|
|
53
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
54
|
+
const MODULES_DIR = "node_modules";
|
|
53
55
|
/** The file's name, the last segment of its path. */
|
|
54
56
|
function basename(path) {
|
|
55
57
|
return path.slice(path.lastIndexOf("/") + 1);
|
|
56
58
|
}
|
|
59
|
+
/** Whether one path is the project's own: below the target root and in no `node_modules`. */
|
|
60
|
+
function isProjectPath(root, path) {
|
|
61
|
+
return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
|
|
62
|
+
}
|
|
57
63
|
function isRecord(value) {
|
|
58
64
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
65
|
}
|
|
@@ -65,6 +71,160 @@ export function configurationModuleForm(name) {
|
|
|
65
71
|
export function isConfigurationDocumentName(name) {
|
|
66
72
|
return DOCUMENT_NAME.test(name);
|
|
67
73
|
}
|
|
74
|
+
/** The flags a tool's command line names its configuration file with. */
|
|
75
|
+
const CONFIG_FLAGS = new Set(["--config", "-c"]);
|
|
76
|
+
/** The extensions of a module a configuration flag may name. */
|
|
77
|
+
const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
78
|
+
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
79
|
+
const BLANK = /[ \t\r\f\v]/u;
|
|
80
|
+
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
81
|
+
const OPERATOR = /[\n;&|()<>]/u;
|
|
82
|
+
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
83
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
84
|
+
/**
|
|
85
|
+
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
86
|
+
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
87
|
+
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
88
|
+
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
89
|
+
* and each operator are undefined, so a flag's file is never read across them.
|
|
90
|
+
*/
|
|
91
|
+
function shellWords(command) {
|
|
92
|
+
const words = [];
|
|
93
|
+
let word;
|
|
94
|
+
let computed = false;
|
|
95
|
+
const end = () => {
|
|
96
|
+
if (word !== undefined) {
|
|
97
|
+
words.push(computed ? undefined : word);
|
|
98
|
+
}
|
|
99
|
+
word = undefined;
|
|
100
|
+
computed = false;
|
|
101
|
+
};
|
|
102
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
103
|
+
const char = command.charAt(at);
|
|
104
|
+
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
105
|
+
at += 1;
|
|
106
|
+
}
|
|
107
|
+
else if (BLANK.test(char)) {
|
|
108
|
+
end();
|
|
109
|
+
}
|
|
110
|
+
else if (OPERATOR.test(char)) {
|
|
111
|
+
end();
|
|
112
|
+
words.push(undefined);
|
|
113
|
+
}
|
|
114
|
+
else if (char === "#" && word === undefined) {
|
|
115
|
+
const newline = command.indexOf("\n", at);
|
|
116
|
+
at = newline < 0 ? command.length : newline - 1;
|
|
117
|
+
}
|
|
118
|
+
else if (char === "\\") {
|
|
119
|
+
at += 1;
|
|
120
|
+
word = (word ?? "") + command.charAt(at);
|
|
121
|
+
}
|
|
122
|
+
else if (char === "'" || char === '"') {
|
|
123
|
+
const close = closingQuote(command, at);
|
|
124
|
+
if (close < 0) {
|
|
125
|
+
word ??= "";
|
|
126
|
+
computed = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
const quoted = char === "'"
|
|
130
|
+
? { text: command.slice(at + 1, close), computed: false }
|
|
131
|
+
: doubleQuoted(command.slice(at + 1, close));
|
|
132
|
+
word = (word ?? "") + quoted.text;
|
|
133
|
+
computed ||= quoted.computed;
|
|
134
|
+
at = close;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
138
|
+
word = (word ?? "") + char;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
end();
|
|
142
|
+
return words;
|
|
143
|
+
}
|
|
144
|
+
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
145
|
+
function doubleQuoted(inner) {
|
|
146
|
+
let text = "";
|
|
147
|
+
let computed = false;
|
|
148
|
+
for (let at = 0; at < inner.length; at += 1) {
|
|
149
|
+
const char = inner.charAt(at);
|
|
150
|
+
const next = inner.charAt(at + 1);
|
|
151
|
+
if (char === "\\" && next === "\n") {
|
|
152
|
+
at += 1;
|
|
153
|
+
}
|
|
154
|
+
else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
155
|
+
text += next;
|
|
156
|
+
at += 1;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
computed ||= char === "$" || char === "`";
|
|
160
|
+
text += char;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { text, computed };
|
|
164
|
+
}
|
|
165
|
+
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
166
|
+
function closingQuote(command, open) {
|
|
167
|
+
const quote = command.charAt(open);
|
|
168
|
+
for (let at = open + 1; at < command.length; at += 1) {
|
|
169
|
+
const char = command.charAt(at);
|
|
170
|
+
if (char === quote) {
|
|
171
|
+
return at;
|
|
172
|
+
}
|
|
173
|
+
if (quote === '"' && char === "\\") {
|
|
174
|
+
at += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The files the scripts of the manifest in one directory name after a configuration flag,
|
|
181
|
+
* `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
|
|
182
|
+
* the target root and in no `node_modules`.
|
|
183
|
+
*/
|
|
184
|
+
function scriptConfigurations(host, root, dir) {
|
|
185
|
+
let scripts;
|
|
186
|
+
try {
|
|
187
|
+
const manifest = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
|
|
188
|
+
scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (!isRecord(scripts)) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const found = [];
|
|
197
|
+
for (const command of Object.values(scripts)) {
|
|
198
|
+
if (typeof command !== "string") {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const words = shellWords(command);
|
|
202
|
+
words.forEach((word, at) => {
|
|
203
|
+
const named = word !== undefined && CONFIG_FLAGS.has(word)
|
|
204
|
+
? words[at + 1]
|
|
205
|
+
: word === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: /^--config=(.+)$/su.exec(word)?.[1];
|
|
208
|
+
const path = named === undefined ? undefined : resolvePath(dir, named);
|
|
209
|
+
if (path !== undefined &&
|
|
210
|
+
[".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
211
|
+
isProjectPath(root, path) &&
|
|
212
|
+
host.kindOf(path) === "file") {
|
|
213
|
+
found.push(path);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
/** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
|
|
220
|
+
function readDocument(host, path) {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(host.readFile(path));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
68
228
|
/** Every string value one decoded JSON value holds at any depth. */
|
|
69
229
|
export function documentStrings(value) {
|
|
70
230
|
if (typeof value === "string") {
|
|
@@ -116,7 +276,8 @@ export function configurationFiles(host, targetRoot, files, parse) {
|
|
|
116
276
|
dirs.add(dir);
|
|
117
277
|
}
|
|
118
278
|
}
|
|
119
|
-
const
|
|
279
|
+
const pending = [];
|
|
280
|
+
const named = [];
|
|
120
281
|
for (const dir of [...dirs].sort()) {
|
|
121
282
|
if (!holdsManifest(dir)) {
|
|
122
283
|
continue;
|
|
@@ -130,24 +291,57 @@ export function configurationFiles(host, targetRoot, files, parse) {
|
|
|
130
291
|
.sort();
|
|
131
292
|
}
|
|
132
293
|
catch {
|
|
133
|
-
|
|
294
|
+
names = [];
|
|
134
295
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
296
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
297
|
+
for (const path of scriptConfigurations(host, root, dir)) {
|
|
298
|
+
const held = files.byName.get(path);
|
|
299
|
+
if (path.endsWith(".json")) {
|
|
300
|
+
pending.push({ path, manifestDir: dir });
|
|
140
301
|
}
|
|
141
|
-
|
|
302
|
+
else if (held !== undefined) {
|
|
303
|
+
modules.push({ file: held, form: "a script's configuration flag" });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
named.push({ path, manifestDir: dir });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// A document reached from two manifests is read against each, so a chain is keyed by both.
|
|
311
|
+
const documents = new Map();
|
|
312
|
+
const unreadable = new Set();
|
|
313
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
314
|
+
let document = documents.get(next.path);
|
|
315
|
+
if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (document === undefined) {
|
|
319
|
+
const value = readDocument(host, next.path);
|
|
320
|
+
if (value === undefined) {
|
|
321
|
+
unreadable.add(next.path);
|
|
142
322
|
continue;
|
|
143
323
|
}
|
|
144
|
-
|
|
324
|
+
document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
|
|
325
|
+
documents.set(next.path, document);
|
|
326
|
+
}
|
|
327
|
+
document.manifestDirs.push(next.manifestDir);
|
|
328
|
+
for (const text of document.strings) {
|
|
329
|
+
const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
|
|
330
|
+
? resolvePath(dirnamePath(next.path), text)
|
|
331
|
+
: undefined;
|
|
332
|
+
if (path?.endsWith(".json") === true &&
|
|
333
|
+
isProjectPath(root, path) &&
|
|
334
|
+
host.kindOf(path) === "file") {
|
|
335
|
+
pending.push({ path, manifestDir: next.manifestDir });
|
|
336
|
+
}
|
|
145
337
|
}
|
|
146
338
|
}
|
|
147
339
|
return {
|
|
148
340
|
modules,
|
|
149
|
-
documents,
|
|
150
|
-
outside: parse === undefined
|
|
341
|
+
documents: [...documents.values()],
|
|
342
|
+
outside: parse === undefined
|
|
343
|
+
? []
|
|
344
|
+
: outsideModules(host, root, [...dirs], files, holdsManifest, parse, named),
|
|
151
345
|
};
|
|
152
346
|
}
|
|
153
347
|
/** The relative specifiers one file's imports and re-exports name. */
|
|
@@ -192,11 +386,11 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
|
|
|
192
386
|
}
|
|
193
387
|
/**
|
|
194
388
|
* The configuration modules of the directories that hold a manifest which the program
|
|
195
|
-
* does not hold, and the files they import by a relative specifier,
|
|
196
|
-
* file joins. A file that cannot be read is skipped.
|
|
389
|
+
* does not hold, and the project files below `root` they import by a relative specifier,
|
|
390
|
+
* until no further file joins. A file that cannot be read is skipped.
|
|
197
391
|
*/
|
|
198
|
-
function outsideModules(host, dirs, files, holdsManifest, parse) {
|
|
199
|
-
const pending = [];
|
|
392
|
+
function outsideModules(host, root, dirs, files, holdsManifest, parse, named) {
|
|
393
|
+
const pending = [...named];
|
|
200
394
|
for (const dir of [...dirs].sort()) {
|
|
201
395
|
if (!holdsManifest(dir)) {
|
|
202
396
|
continue;
|
|
@@ -214,30 +408,36 @@ function outsideModules(host, dirs, files, holdsManifest, parse) {
|
|
|
214
408
|
}
|
|
215
409
|
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
216
410
|
}
|
|
217
|
-
const found =
|
|
218
|
-
const
|
|
411
|
+
const found = new Map();
|
|
412
|
+
const unreadable = new Set();
|
|
219
413
|
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
220
|
-
|
|
414
|
+
let outside = found.get(next.path);
|
|
415
|
+
if (unreadable.has(next.path) ||
|
|
416
|
+
files.byName.has(next.path) ||
|
|
417
|
+
outside?.manifestDirs.includes(next.manifestDir) === true) {
|
|
221
418
|
continue;
|
|
222
419
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
420
|
+
if (outside === undefined) {
|
|
421
|
+
let text;
|
|
422
|
+
try {
|
|
423
|
+
text = host.readFile(next.path);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
unreadable.add(next.path);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
outside = { file: parse(next.path, text), manifestDirs: [] };
|
|
430
|
+
found.set(next.path, outside);
|
|
230
431
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const specifier of relativeSpecifiers(file)) {
|
|
432
|
+
outside.manifestDirs.push(next.manifestDir);
|
|
433
|
+
for (const specifier of relativeSpecifiers(outside.file)) {
|
|
234
434
|
const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
|
|
235
|
-
if (reached !== undefined) {
|
|
435
|
+
if (reached !== undefined && isProjectPath(root, reached)) {
|
|
236
436
|
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
237
437
|
}
|
|
238
438
|
}
|
|
239
439
|
}
|
|
240
|
-
return found;
|
|
440
|
+
return [...found.values()];
|
|
241
441
|
}
|
|
242
442
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|
|
243
443
|
export function declaredIn(host, dir) {
|
|
@@ -4,7 +4,7 @@ export const RECORD = {
|
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "5.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
};
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* An object-literal property writes the member of its name on each constituent of the
|
|
4
4
|
* literal's contextual type that declares one. A value reaching a position typed by another
|
|
5
5
|
* object type reads each of its members that type declares, through array elements and
|
|
6
|
-
*
|
|
7
|
-
* interface-satisfaction class's evidence instead.
|
|
6
|
+
* the parameters and results of functions and constructors too; a class instance there is
|
|
7
|
+
* the interface-satisfaction class's evidence instead.
|
|
8
8
|
*/
|
|
9
|
-
import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { isClassDeclaration, isClassExpression, isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
10
10
|
import { SignatureKind } from "@typescript/native/unstable/sync";
|
|
11
11
|
import { isAnswered } from "./query.js";
|
|
12
12
|
/** The member kinds a contextual type writes and reads: data members and the members of object types. */
|
|
@@ -36,6 +36,20 @@ function propertyName(property) {
|
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether an object spread copies a member a declaration declares: a method, a method
|
|
41
|
+
* signature and a class's accessor are no own data property of the value, while an
|
|
42
|
+
* accessor of an interface or a type literal describes one. It is decided on the
|
|
43
|
+
* declaration, because a mapped type over a class keeps the class's declarations on
|
|
44
|
+
* plain property symbols.
|
|
45
|
+
*/
|
|
46
|
+
function copiedBySpread(node) {
|
|
47
|
+
if (isMethodDeclaration(node) || isMethodSignatureDeclaration(node)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return !((isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) &&
|
|
51
|
+
(isClassDeclaration(node.parent) || isClassExpression(node.parent)));
|
|
52
|
+
}
|
|
39
53
|
/**
|
|
40
54
|
* The contextual uses of one project's literals and values, asked in batches: one
|
|
41
55
|
* contextual type per literal and per value whose type carries a member of the target,
|
|
@@ -45,6 +59,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
45
59
|
const queries = project.queries;
|
|
46
60
|
const ownPaths = project.ownPaths();
|
|
47
61
|
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
62
|
+
const privateNames = new Set(held.symbols
|
|
63
|
+
.filter((symbol) => symbol.visibility === "private-name")
|
|
64
|
+
.map((symbol) => symbol.id));
|
|
48
65
|
let batches = 0;
|
|
49
66
|
let lookups = 0;
|
|
50
67
|
const batchesOf = (count) => Math.ceil(count / cap);
|
|
@@ -82,8 +99,12 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
82
99
|
ids: chains
|
|
83
100
|
.declarationsOf(property)
|
|
84
101
|
.filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
|
|
102
|
+
methods: chains
|
|
103
|
+
.declarationsOf(property)
|
|
104
|
+
.filter((id) => kinds.get(id) === "interface-method"),
|
|
85
105
|
handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
|
|
86
106
|
own: property.declarations.some((handle) => ownPaths.has(handle.path)),
|
|
107
|
+
declarations: property.declarations.filter((handle) => ownPaths.has(handle.path)),
|
|
87
108
|
});
|
|
88
109
|
}
|
|
89
110
|
tables.set(type.id, table);
|
|
@@ -133,7 +154,10 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
133
154
|
batches += batchesOf(carried.length);
|
|
134
155
|
const types = queries.typesAt(carried.map((flow) => flow.value), cap);
|
|
135
156
|
/** The members a position may read structurally: those of object types, not of classes. */
|
|
136
|
-
const readable = (member) =>
|
|
157
|
+
const readable = (member) => [
|
|
158
|
+
...member.ids.filter((id) => kinds.get(id) === "type-member"),
|
|
159
|
+
...member.methods,
|
|
160
|
+
];
|
|
137
161
|
const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
|
|
138
162
|
/** Whether an object type has members and declares none of them in the target. */
|
|
139
163
|
const outside = (part) => {
|
|
@@ -157,14 +181,36 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
157
181
|
elements.set(type.id, element);
|
|
158
182
|
return element;
|
|
159
183
|
};
|
|
184
|
+
const argumentsOf = new Map();
|
|
185
|
+
/** The type arguments of an instantiation of a generic type, read once per type. */
|
|
186
|
+
const typeArgumentsOf = (type) => {
|
|
187
|
+
const known = argumentsOf.get(type.id);
|
|
188
|
+
if (known !== undefined) {
|
|
189
|
+
return known;
|
|
190
|
+
}
|
|
191
|
+
lookups += type.isTypeReference() ? 1 : 0;
|
|
192
|
+
const held = queries.typeArguments(type);
|
|
193
|
+
const found = isAnswered(held) ? held : [];
|
|
194
|
+
argumentsOf.set(type.id, found);
|
|
195
|
+
return found;
|
|
196
|
+
};
|
|
160
197
|
const calls = new Map();
|
|
161
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* The parameter and return types of a type's one call signature, or of its one construct
|
|
200
|
+
* signature where it has no call signature, read once per type.
|
|
201
|
+
*/
|
|
162
202
|
const callOf = (type) => {
|
|
163
203
|
if (calls.has(type.id)) {
|
|
164
204
|
return calls.get(type.id);
|
|
165
205
|
}
|
|
166
206
|
lookups += 1;
|
|
167
|
-
|
|
207
|
+
let kind = SignatureKind.Call;
|
|
208
|
+
let signatures = queries.signaturesOf(type, kind);
|
|
209
|
+
if (isAnswered(signatures) && signatures.length === 0) {
|
|
210
|
+
lookups += 1;
|
|
211
|
+
kind = SignatureKind.Construct;
|
|
212
|
+
signatures = queries.signaturesOf(type, kind);
|
|
213
|
+
}
|
|
168
214
|
let called;
|
|
169
215
|
const [signature, ...more] = isAnswered(signatures) ? signatures : [];
|
|
170
216
|
if (signature !== undefined && more.length === 0) {
|
|
@@ -172,6 +218,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
172
218
|
const result = queries.returnTypeOf(signature);
|
|
173
219
|
lookups += parameters.length + 1;
|
|
174
220
|
called = {
|
|
221
|
+
kind,
|
|
175
222
|
parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
|
|
176
223
|
result: isAnswered(result) ? result : undefined,
|
|
177
224
|
};
|
|
@@ -181,10 +228,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
181
228
|
};
|
|
182
229
|
/**
|
|
183
230
|
* Each object type a value of `source` is checked as against the object types of
|
|
184
|
-
* `target`: the value's own, and through array elements
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
231
|
+
* `target`: the value's own, and through array elements, a function's parameters,
|
|
232
|
+
* checked the other way, and result, and the type arguments two instantiations share
|
|
233
|
+
* by place, an array among them, as an array reaches `Iterable<T>`.
|
|
234
|
+
* A source that is one of the target's own object types matches it as it stands. Each
|
|
235
|
+
* pair is compared once, which ends the walk over a recursive type.
|
|
188
236
|
*/
|
|
189
237
|
const compared = (source, target, into, seen) => {
|
|
190
238
|
const key = `${String(source?.id)}>${String(target?.id)}`;
|
|
@@ -206,13 +254,24 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
206
254
|
}
|
|
207
255
|
const element = elementOf(one);
|
|
208
256
|
const called = element === undefined ? callOf(one) : undefined;
|
|
257
|
+
const sourceArguments = typeArgumentsOf(one);
|
|
209
258
|
for (const part of targets) {
|
|
259
|
+
const partElement = elementOf(part);
|
|
260
|
+
if (element !== undefined && partElement !== undefined) {
|
|
261
|
+
compared(element, partElement, into, seen);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const targetArguments = typeArgumentsOf(part);
|
|
265
|
+
// A defaulted trailing parameter, as `Iterable<T, TReturn, TNext>` has, leaves
|
|
266
|
+
// the places two instantiations share comparable.
|
|
267
|
+
sourceArguments.slice(0, targetArguments.length).forEach((argument, index) => {
|
|
268
|
+
compared(argument, targetArguments[index], into, seen);
|
|
269
|
+
});
|
|
210
270
|
if (element !== undefined) {
|
|
211
|
-
compared(element, elementOf(part), into, seen);
|
|
212
271
|
continue;
|
|
213
272
|
}
|
|
214
273
|
const other = called === undefined ? undefined : callOf(part);
|
|
215
|
-
if (called === undefined || other
|
|
274
|
+
if (called === undefined || other?.kind !== called.kind) {
|
|
216
275
|
continue;
|
|
217
276
|
}
|
|
218
277
|
other.parameters.forEach((parameter, index) => {
|
|
@@ -239,7 +298,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
239
298
|
if (element !== undefined) {
|
|
240
299
|
return mayCarry(element, seen);
|
|
241
300
|
}
|
|
242
|
-
return callOf(part) !== undefined;
|
|
301
|
+
return callOf(part) !== undefined || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
|
|
243
302
|
});
|
|
244
303
|
};
|
|
245
304
|
const sourced = carried.flatMap((flow, index) => {
|
|
@@ -288,5 +347,37 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
288
347
|
reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
|
|
289
348
|
}
|
|
290
349
|
});
|
|
350
|
+
const copies = new Map();
|
|
351
|
+
/** Whether a spread copies the member: where any declaration of it declares data. */
|
|
352
|
+
const copied = (member) => {
|
|
353
|
+
let known = copies.get(member);
|
|
354
|
+
if (known === undefined) {
|
|
355
|
+
known = member.declarations.some((handle) => {
|
|
356
|
+
const node = project.declarationAt(handle)?.node;
|
|
357
|
+
return node === undefined || copiedBySpread(node);
|
|
358
|
+
});
|
|
359
|
+
copies.set(member, known);
|
|
360
|
+
}
|
|
361
|
+
return known;
|
|
362
|
+
};
|
|
363
|
+
const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
|
|
364
|
+
batches += batchesOf(spreads.length);
|
|
365
|
+
const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
|
|
366
|
+
spreads.forEach(({ value, from }, index) => {
|
|
367
|
+
const type = spreadTypes[index];
|
|
368
|
+
if (type === undefined || !isAnswered(type)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// A spread gets every own enumerable property, an overwritten key's getter included,
|
|
372
|
+
// and no private name, which is no property
|
|
373
|
+
// (https://tc39.es/ecma262/#sec-copydataproperties).
|
|
374
|
+
const own = (member, id) => !privateNames.has(id) && copied(member);
|
|
375
|
+
const ids = [
|
|
376
|
+
...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
|
|
377
|
+
];
|
|
378
|
+
if (ids.length > 0) {
|
|
379
|
+
reads.push({ at: value, from, ids });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
291
382
|
return { writes, reads, batches, lookups };
|
|
292
383
|
}
|
|
@@ -39,6 +39,12 @@ const BABEL_CONFIG = [
|
|
|
39
39
|
const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
|
|
40
40
|
const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
|
|
41
41
|
const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
|
|
42
|
+
/**
|
|
43
|
+
* The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
|
|
44
|
+
* resolves `setupFiles` and `globalSetup` against that root
|
|
45
|
+
* (https://vitest.dev/config/setupfiles, https://vitest.dev/config/globalsetup).
|
|
46
|
+
*/
|
|
47
|
+
const VITEST_ROOTS = ["test.root", "root"];
|
|
42
48
|
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
43
49
|
const ANGULAR_ENTRIES = [
|
|
44
50
|
"src/{main,main.server,server,polyfills,test}.ts",
|
|
@@ -535,5 +541,9 @@ export const CONVENTION_ROWS = [
|
|
|
535
541
|
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
536
542
|
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
537
543
|
],
|
|
544
|
+
testFiles: [
|
|
545
|
+
{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
|
|
546
|
+
{ property: "test.globalSetup", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
|
|
547
|
+
],
|
|
538
548
|
},
|
|
539
549
|
];
|
package/dist/src/conventions.js
CHANGED
|
@@ -413,6 +413,35 @@ function holdsValue(held, value) {
|
|
|
413
413
|
return first !== undefined && literalText(first) === value;
|
|
414
414
|
}));
|
|
415
415
|
}
|
|
416
|
+
/** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
|
|
417
|
+
function testFileRoot(file, dir, reading) {
|
|
418
|
+
for (const property of reading.roots) {
|
|
419
|
+
const values = valuesAt(file, property, reading.call);
|
|
420
|
+
if (values.length > 0) {
|
|
421
|
+
const texts = new Set(values.map(literalText));
|
|
422
|
+
const [text] = texts;
|
|
423
|
+
return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return dir;
|
|
427
|
+
}
|
|
428
|
+
/** The files an applied row's test-file properties name. */
|
|
429
|
+
function testFilesNamed(host, parse, dir, readings, into) {
|
|
430
|
+
for (const reading of readings) {
|
|
431
|
+
for (const { file } of readingFiles(host, parse, dir, reading.files)) {
|
|
432
|
+
const at = testFileRoot(file, dir, reading);
|
|
433
|
+
if (at === undefined) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
for (const node of stringsAt(file, reading.property, reading.call)) {
|
|
437
|
+
const text = literalText(node);
|
|
438
|
+
if (text !== undefined && text !== "") {
|
|
439
|
+
into.add(resolvePath(at, text));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
416
445
|
/** The strings an applied row's selection keys hold, added per configuration file to `into`. */
|
|
417
446
|
function selectedStrings(host, parse, dir, readings, into) {
|
|
418
447
|
for (const reading of readings) {
|
|
@@ -664,6 +693,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
664
693
|
const failures = [];
|
|
665
694
|
const uses = new Set();
|
|
666
695
|
const selected = new Map();
|
|
696
|
+
const testFiles = new Set();
|
|
667
697
|
for (const manifest of manifests) {
|
|
668
698
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
669
699
|
const versions = new Map();
|
|
@@ -677,7 +707,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
677
707
|
}
|
|
678
708
|
const version = versions.get(row.package);
|
|
679
709
|
if (version === undefined) {
|
|
680
|
-
if (missing.has(row.package) ||
|
|
710
|
+
if (missing.has(row.package) ||
|
|
711
|
+
manifest.nested === true ||
|
|
712
|
+
!declares(declared, row.package, REQUIRED_SECTIONS)) {
|
|
681
713
|
continue;
|
|
682
714
|
}
|
|
683
715
|
missing.add(row.package);
|
|
@@ -735,6 +767,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
735
767
|
uses.add(name);
|
|
736
768
|
}
|
|
737
769
|
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
770
|
+
testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
|
|
738
771
|
const excludes = [
|
|
739
772
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
740
773
|
].map(globExpression);
|
|
@@ -762,6 +795,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
762
795
|
failures,
|
|
763
796
|
uses: [...uses].sort(compare),
|
|
764
797
|
selected,
|
|
798
|
+
testFiles,
|
|
765
799
|
};
|
|
766
800
|
}
|
|
767
801
|
/**
|