@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.2
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 +6 -16
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/contextual-uses.js +41 -7
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +32 -0
- package/dist/src/dependencies.js +12 -8
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +35 -4
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/indexed-access.js +8 -2
- package/dist/src/inventory.js +3 -12
- package/dist/src/references.js +2 -2
- package/dist/src/report.js +3 -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/session.js +0 -1
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- 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 +2 -1
- package/src/analysis.ts +10 -30
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +1 -1
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +246 -28
- package/src/contextual-uses.ts +63 -9
- package/src/convention-rows.ts +19 -0
- package/src/conventions.ts +43 -0
- package/src/dependencies.ts +14 -10
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/entry-points.ts +1 -1
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +1 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +46 -4
- package/src/findings/emitters.ts +3 -8
- package/src/findings/self-check.ts +17 -14
- package/src/findings-pass.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/indexed-access.ts +10 -2
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +7 -6
- package/src/report.ts +8 -4
- 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/session.ts +0 -2
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/template-field.ts +1 -1
- package/src/value-reach.ts +1 -1
- 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
package/dist/src/analysis.js
CHANGED
|
@@ -23,7 +23,7 @@ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhi
|
|
|
23
23
|
import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
|
|
24
24
|
import { globExpression } from "./glob.js";
|
|
25
25
|
import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
|
|
26
|
-
import {
|
|
26
|
+
import { unmatchedRootFindings } from "./findings/self-check.js";
|
|
27
27
|
import { frameworkLifecycle } from "./framework-lifecycle.js";
|
|
28
28
|
import { implementations, mergeImplementations } from "./implementations.js";
|
|
29
29
|
import { injectionContainer } from "./injection-container.js";
|
|
@@ -243,6 +243,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
243
243
|
.filter((file) => referenceOnly(file.fileName))
|
|
244
244
|
.flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
|
|
245
245
|
generated: generatedFilesOf(own, generatedFiles(conventions, own)),
|
|
246
|
+
testFileNames: conventions.testFiles,
|
|
246
247
|
};
|
|
247
248
|
};
|
|
248
249
|
for (const file of project.ownSourceFiles()) {
|
|
@@ -346,7 +347,7 @@ export function runRoots(engine, host, scope, config, provenance) {
|
|
|
346
347
|
};
|
|
347
348
|
}
|
|
348
349
|
/** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
|
|
349
|
-
const DETECTORS = new Map([
|
|
350
|
+
export const DETECTORS = new Map([
|
|
350
351
|
["interface-satisfaction", interfaceSatisfaction],
|
|
351
352
|
["enum-group", enumGroup],
|
|
352
353
|
["generated-file", generatedFile],
|
|
@@ -374,6 +375,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
374
375
|
const projectRead = opened.read();
|
|
375
376
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
376
377
|
testFiles: config.ts.testFiles,
|
|
378
|
+
testFileNames: projectRead.testFileNames,
|
|
377
379
|
});
|
|
378
380
|
const evidence = evidenceRecords({
|
|
379
381
|
project,
|
|
@@ -419,7 +421,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
|
|
|
419
421
|
const { projectRead } = one;
|
|
420
422
|
const satisfied = settled[index];
|
|
421
423
|
const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
|
|
422
|
-
disabled,
|
|
423
424
|
mode,
|
|
424
425
|
testFiles: new Set([...one.testFiles, ...support]),
|
|
425
426
|
});
|
|
@@ -623,7 +624,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
|
|
|
623
624
|
return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
|
|
624
625
|
}
|
|
625
626
|
/** Reads a run's projects with every per-project fact an emitter reads. */
|
|
626
|
-
function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
627
|
+
export function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
627
628
|
const targetRoot = scope.target.path;
|
|
628
629
|
const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
|
|
629
630
|
deprecation: deprecatedDeclarations(project, read.held),
|
|
@@ -682,7 +683,7 @@ function nearestManifestDir(host, root, file) {
|
|
|
682
683
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
683
684
|
* each kind family reads, every per-project one read in the same session as the sweep.
|
|
684
685
|
*/
|
|
685
|
-
function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
686
|
+
export function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
686
687
|
const targetRoot = scope.target.path;
|
|
687
688
|
const swept = sweptOf(read, input);
|
|
688
689
|
const { projects } = read;
|
|
@@ -714,17 +715,6 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
714
715
|
intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
|
|
715
716
|
};
|
|
716
717
|
}
|
|
717
|
-
/**
|
|
718
|
-
* What every emitter reads of the run the scope and the configuration describe, swept
|
|
719
|
-
* under the caller's marks: the run's sweep, the facts beside it each kind family reads,
|
|
720
|
-
* and the sides of the target's declared cross-language edges that name a symbol of this
|
|
721
|
-
* language. The run reads no suppression document, so the self-check family has nothing
|
|
722
|
-
* to report.
|
|
723
|
-
*/
|
|
724
|
-
export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
|
|
725
|
-
const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
|
|
726
|
-
return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
|
|
727
|
-
}
|
|
728
718
|
/**
|
|
729
719
|
* Reads the run the scope and the configuration describe for its findings. The inline
|
|
730
720
|
* directives, the ignore file's entries and the baseline's rows are bound before any
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { dirnamePath, joinPath } from "./paths.js";
|
|
9
9
|
/** The name the mapper's manifest declares and every configuration's entry names. */
|
|
10
|
-
|
|
10
|
+
const MAPPER_PACKAGE = "deadset-ts-component-files";
|
|
11
11
|
/** The manifest of the mapper's package, which runs `command`. */
|
|
12
12
|
export function mapperManifest(command) {
|
|
13
13
|
return `${JSON.stringify({
|
|
@@ -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, [...dirs], files, holdsManifest, parse, named),
|
|
151
345
|
};
|
|
152
346
|
}
|
|
153
347
|
/** The relative specifiers one file's imports and re-exports name. */
|
|
@@ -195,8 +389,8 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
|
|
|
195
389
|
* does not hold, and the files they import by a relative specifier, until no further
|
|
196
390
|
* 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, 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
435
|
if (reached !== undefined) {
|
|
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) {
|
|
@@ -3,11 +3,11 @@
|
|
|
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";
|
|
10
|
-
import { SignatureKind } from "@typescript/native/unstable/sync";
|
|
9
|
+
import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
10
|
+
import { SignatureKind, SymbolFlags } 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. */
|
|
13
13
|
const MEMBER_KINDS = new Set(["type-member", "class-member"]);
|
|
@@ -45,6 +45,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
45
45
|
const queries = project.queries;
|
|
46
46
|
const ownPaths = project.ownPaths();
|
|
47
47
|
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
48
|
+
const privateNames = new Set(held.symbols
|
|
49
|
+
.filter((symbol) => symbol.visibility === "private-name")
|
|
50
|
+
.map((symbol) => symbol.id));
|
|
48
51
|
let batches = 0;
|
|
49
52
|
let lookups = 0;
|
|
50
53
|
const batchesOf = (count) => Math.ceil(count / cap);
|
|
@@ -84,6 +87,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
84
87
|
.filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
|
|
85
88
|
handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
|
|
86
89
|
own: property.declarations.some((handle) => ownPaths.has(handle.path)),
|
|
90
|
+
accessor: (property.flags & SymbolFlags.Accessor) !== 0,
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
tables.set(type.id, table);
|
|
@@ -158,13 +162,22 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
158
162
|
return element;
|
|
159
163
|
};
|
|
160
164
|
const calls = new Map();
|
|
161
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* The parameter and return types of a type's one call signature, or of its one construct
|
|
167
|
+
* signature where it has no call signature, read once per type.
|
|
168
|
+
*/
|
|
162
169
|
const callOf = (type) => {
|
|
163
170
|
if (calls.has(type.id)) {
|
|
164
171
|
return calls.get(type.id);
|
|
165
172
|
}
|
|
166
173
|
lookups += 1;
|
|
167
|
-
|
|
174
|
+
let kind = SignatureKind.Call;
|
|
175
|
+
let signatures = queries.signaturesOf(type, kind);
|
|
176
|
+
if (isAnswered(signatures) && signatures.length === 0) {
|
|
177
|
+
lookups += 1;
|
|
178
|
+
kind = SignatureKind.Construct;
|
|
179
|
+
signatures = queries.signaturesOf(type, kind);
|
|
180
|
+
}
|
|
168
181
|
let called;
|
|
169
182
|
const [signature, ...more] = isAnswered(signatures) ? signatures : [];
|
|
170
183
|
if (signature !== undefined && more.length === 0) {
|
|
@@ -172,6 +185,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
172
185
|
const result = queries.returnTypeOf(signature);
|
|
173
186
|
lookups += parameters.length + 1;
|
|
174
187
|
called = {
|
|
188
|
+
kind,
|
|
175
189
|
parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
|
|
176
190
|
result: isAnswered(result) ? result : undefined,
|
|
177
191
|
};
|
|
@@ -212,7 +226,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
212
226
|
continue;
|
|
213
227
|
}
|
|
214
228
|
const other = called === undefined ? undefined : callOf(part);
|
|
215
|
-
if (called === undefined || other
|
|
229
|
+
if (called === undefined || other?.kind !== called.kind) {
|
|
216
230
|
continue;
|
|
217
231
|
}
|
|
218
232
|
other.parameters.forEach((parameter, index) => {
|
|
@@ -288,5 +302,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
288
302
|
reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
|
|
289
303
|
}
|
|
290
304
|
});
|
|
305
|
+
const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
|
|
306
|
+
batches += batchesOf(spreads.length);
|
|
307
|
+
const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
|
|
308
|
+
spreads.forEach(({ value, from }, index) => {
|
|
309
|
+
const type = spreadTypes[index];
|
|
310
|
+
if (type === undefined || !isAnswered(type)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// A spread gets every own enumerable property, an overwritten key's getter included.
|
|
314
|
+
// So it reads each data member but a class's accessor, which sits on the prototype with
|
|
315
|
+
// the class's methods (never data members), and a private name, which is no property
|
|
316
|
+
// (https://tc39.es/ecma262/#sec-copydataproperties).
|
|
317
|
+
const own = (member, id) => !privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
|
|
318
|
+
const ids = [
|
|
319
|
+
...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
|
|
320
|
+
];
|
|
321
|
+
if (ids.length > 0) {
|
|
322
|
+
reads.push({ at: value, from, ids });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
291
325
|
return { writes, reads, batches, lookups };
|
|
292
326
|
}
|
|
@@ -39,6 +39,11 @@ 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` against that root (https://vitest.dev/config/setupfiles).
|
|
45
|
+
*/
|
|
46
|
+
const VITEST_ROOTS = ["test.root", "root"];
|
|
42
47
|
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
43
48
|
const ANGULAR_ENTRIES = [
|
|
44
49
|
"src/{main,main.server,server,polyfills,test}.ts",
|
|
@@ -535,5 +540,6 @@ export const CONVENTION_ROWS = [
|
|
|
535
540
|
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
536
541
|
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
537
542
|
],
|
|
543
|
+
testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
|
|
538
544
|
},
|
|
539
545
|
];
|
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();
|
|
@@ -735,6 +765,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
735
765
|
uses.add(name);
|
|
736
766
|
}
|
|
737
767
|
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
768
|
+
testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
|
|
738
769
|
const excludes = [
|
|
739
770
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
740
771
|
].map(globExpression);
|
|
@@ -762,6 +793,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
762
793
|
failures,
|
|
763
794
|
uses: [...uses].sort(compare),
|
|
764
795
|
selected,
|
|
796
|
+
testFiles,
|
|
765
797
|
};
|
|
766
798
|
}
|
|
767
799
|
/**
|
package/dist/src/dependencies.js
CHANGED
|
@@ -244,16 +244,20 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
|
|
|
244
244
|
use(ownersOf(file, held, node), dependenciesNamed(text, names));
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
for (const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
for (const { strings, manifestDirs } of configuration.documents) {
|
|
248
|
+
for (const manifestDir of manifestDirs) {
|
|
249
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
250
|
+
for (const text of strings) {
|
|
251
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
252
|
+
}
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
|
-
for (const { file,
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
for (const { file, manifestDirs } of configuration.outside) {
|
|
256
|
+
for (const manifestDir of manifestDirs) {
|
|
257
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
258
|
+
for (const { text } of moduleStrings(file)) {
|
|
259
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
260
|
+
}
|
|
257
261
|
}
|
|
258
262
|
}
|
|
259
263
|
return { packages, uses, unanswered };
|
package/dist/src/discover.js
CHANGED
|
@@ -23,7 +23,7 @@ export class DiscoveryError extends Error {
|
|
|
23
23
|
/** The category an error diagnostic carries, which the protocol sends as a number. */
|
|
24
24
|
const ERROR_CATEGORY = DiagnosticCategory.Error;
|
|
25
25
|
/** Whether one diagnostic is an error, which is what fails a run closed. */
|
|
26
|
-
|
|
26
|
+
function isError(diagnostic) {
|
|
27
27
|
return diagnostic.category === ERROR_CATEGORY;
|
|
28
28
|
}
|
|
29
29
|
/**
|
package/dist/src/edges.js
CHANGED
|
@@ -25,7 +25,7 @@ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and a
|
|
|
25
25
|
const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
26
26
|
const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
27
27
|
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
28
|
-
|
|
28
|
+
class EdgesError extends ConfigError {
|
|
29
29
|
constructor(at, text, want) {
|
|
30
30
|
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
31
31
|
this.name = "EdgesError";
|