@cplieger/deadset-ts 6.1.1-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 +45 -32
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +68 -10
- package/dist/src/convention-rows.js +48 -0
- package/dist/src/conventions.js +80 -0
- package/dist/src/dependencies.js +121 -46
- 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 +41 -11
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +377 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/inventory.js +3 -12
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +62 -23
- package/dist/src/report.js +3 -0
- package/dist/src/roots.js +96 -7
- 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 +6 -2
- package/dist/src/source-files.js +1 -0
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -4
- 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 +55 -47
- 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/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +99 -13
- package/src/convention-rows.ts +76 -0
- package/src/conventions.ts +102 -0
- package/src/dependencies.ts +148 -56
- 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 +5 -1
- package/src/findings/completion.ts +53 -11
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- 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 +34 -16
- package/src/indexed-access.ts +476 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/kinds.ts +7 -6
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +93 -33
- package/src/report.ts +8 -4
- package/src/roots.ts +129 -9
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/session.ts +7 -3
- package/src/source-files.ts +3 -0
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/template-field.ts +1 -1
- package/src/test-joins.ts +120 -0
- package/src/value-reach.ts +1 -1
- package/src/verbs/verb.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +3 -10
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -3,15 +3,18 @@
|
|
|
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"]);
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The name one literal property is written under: its literal text, or the computed name
|
|
16
|
+
* node whose key the checker names, a unique symbol among them.
|
|
17
|
+
*/
|
|
15
18
|
function propertyName(property) {
|
|
16
19
|
if (!isPropertyAssignment(property) &&
|
|
17
20
|
!isShorthandPropertyAssignment(property) &&
|
|
@@ -29,6 +32,7 @@ function propertyName(property) {
|
|
|
29
32
|
if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
|
|
30
33
|
return { node: name, text: key.text };
|
|
31
34
|
}
|
|
35
|
+
return { node: name, text: undefined };
|
|
32
36
|
}
|
|
33
37
|
return undefined;
|
|
34
38
|
}
|
|
@@ -41,6 +45,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
41
45
|
const queries = project.queries;
|
|
42
46
|
const ownPaths = project.ownPaths();
|
|
43
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));
|
|
44
51
|
let batches = 0;
|
|
45
52
|
let lookups = 0;
|
|
46
53
|
const batchesOf = (count) => Math.ceil(count / cap);
|
|
@@ -80,6 +87,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
80
87
|
.filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
|
|
81
88
|
handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
|
|
82
89
|
own: property.declarations.some((handle) => ownPaths.has(handle.path)),
|
|
90
|
+
accessor: (property.flags & SymbolFlags.Accessor) !== 0,
|
|
83
91
|
});
|
|
84
92
|
}
|
|
85
93
|
tables.set(type.id, table);
|
|
@@ -88,6 +96,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
88
96
|
const writes = [];
|
|
89
97
|
batches += batchesOf(literals.length);
|
|
90
98
|
const contexts = queries.contextualTypes(literals.map((one) => one.literal), cap);
|
|
99
|
+
// A computed name that is not literal text names the property the literal declares
|
|
100
|
+
// under the checker's own key, which is how a type declares a member a unique symbol
|
|
101
|
+
// keys: asked once, for the literals that have a contextual type.
|
|
102
|
+
const computed = literals.flatMap(({ literal }, index) => contexts[index] === undefined || !isAnswered(contexts[index])
|
|
103
|
+
? []
|
|
104
|
+
: literal.properties.flatMap((property) => {
|
|
105
|
+
const name = propertyName(property);
|
|
106
|
+
return name !== undefined && name.text === undefined ? [name.node] : [];
|
|
107
|
+
}));
|
|
108
|
+
batches += batchesOf(computed.length);
|
|
109
|
+
const keys = new Map();
|
|
110
|
+
(computed.length === 0
|
|
111
|
+
? []
|
|
112
|
+
: project.symbolsAt(computed.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
|
|
113
|
+
const node = computed[index];
|
|
114
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
115
|
+
keys.set(node, symbol.name);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
91
118
|
literals.forEach(({ literal, from }, index) => {
|
|
92
119
|
const context = contexts[index];
|
|
93
120
|
if (context === undefined || !isAnswered(context)) {
|
|
@@ -96,10 +123,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
96
123
|
const tablesOf = partsOf(context).map(membersOf);
|
|
97
124
|
for (const property of literal.properties) {
|
|
98
125
|
const name = propertyName(property);
|
|
99
|
-
|
|
126
|
+
const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
|
|
127
|
+
if (name === undefined || text === undefined) {
|
|
100
128
|
continue;
|
|
101
129
|
}
|
|
102
|
-
const ids = [...new Set(tablesOf.flatMap((table) => table.get(
|
|
130
|
+
const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
|
|
103
131
|
if (ids.length > 0) {
|
|
104
132
|
writes.push({ at: name.node, from, ids });
|
|
105
133
|
}
|
|
@@ -134,13 +162,22 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
134
162
|
return element;
|
|
135
163
|
};
|
|
136
164
|
const calls = new Map();
|
|
137
|
-
/**
|
|
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
|
+
*/
|
|
138
169
|
const callOf = (type) => {
|
|
139
170
|
if (calls.has(type.id)) {
|
|
140
171
|
return calls.get(type.id);
|
|
141
172
|
}
|
|
142
173
|
lookups += 1;
|
|
143
|
-
|
|
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
|
+
}
|
|
144
181
|
let called;
|
|
145
182
|
const [signature, ...more] = isAnswered(signatures) ? signatures : [];
|
|
146
183
|
if (signature !== undefined && more.length === 0) {
|
|
@@ -148,6 +185,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
148
185
|
const result = queries.returnTypeOf(signature);
|
|
149
186
|
lookups += parameters.length + 1;
|
|
150
187
|
called = {
|
|
188
|
+
kind,
|
|
151
189
|
parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
|
|
152
190
|
result: isAnswered(result) ? result : undefined,
|
|
153
191
|
};
|
|
@@ -188,7 +226,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
188
226
|
continue;
|
|
189
227
|
}
|
|
190
228
|
const other = called === undefined ? undefined : callOf(part);
|
|
191
|
-
if (called === undefined || other
|
|
229
|
+
if (called === undefined || other?.kind !== called.kind) {
|
|
192
230
|
continue;
|
|
193
231
|
}
|
|
194
232
|
other.parameters.forEach((parameter, index) => {
|
|
@@ -264,5 +302,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
264
302
|
reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
|
|
265
303
|
}
|
|
266
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
|
+
});
|
|
267
325
|
return { writes, reads, batches, lookups };
|
|
268
326
|
}
|
|
@@ -39,8 +39,43 @@ 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"];
|
|
47
|
+
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
48
|
+
const ANGULAR_ENTRIES = [
|
|
49
|
+
"src/{main,main.server,server,polyfills,test}.ts",
|
|
50
|
+
"server.ts",
|
|
51
|
+
"projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
|
|
52
|
+
"projects/*/server.ts",
|
|
53
|
+
];
|
|
54
|
+
/** The `angular.json` members whose strings name the packages a build loads. */
|
|
55
|
+
const ANGULAR_PACKAGE_KEYS = [
|
|
56
|
+
"builder",
|
|
57
|
+
"polyfills",
|
|
58
|
+
"styles",
|
|
59
|
+
"scripts",
|
|
60
|
+
].map((key) => ({ key, files: ["angular.json"] }));
|
|
42
61
|
/** Every row, ordered by name and then by range. */
|
|
43
62
|
export const CONVENTION_ROWS = [
|
|
63
|
+
{
|
|
64
|
+
name: "angular",
|
|
65
|
+
package: "@angular/build",
|
|
66
|
+
range: ">=17.0.0 <22.0.0",
|
|
67
|
+
entries: ANGULAR_ENTRIES,
|
|
68
|
+
moves: [],
|
|
69
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "angular-devkit",
|
|
73
|
+
package: "@angular-devkit/build-angular",
|
|
74
|
+
range: ">=12.0.0 <22.0.0",
|
|
75
|
+
entries: ANGULAR_ENTRIES,
|
|
76
|
+
moves: [],
|
|
77
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
78
|
+
},
|
|
44
79
|
{
|
|
45
80
|
name: "astro",
|
|
46
81
|
package: "astro",
|
|
@@ -472,6 +507,18 @@ export const CONVENTION_ROWS = [
|
|
|
472
507
|
},
|
|
473
508
|
],
|
|
474
509
|
},
|
|
510
|
+
{
|
|
511
|
+
name: "vitepress",
|
|
512
|
+
package: "vitepress",
|
|
513
|
+
range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
|
|
514
|
+
entries: [
|
|
515
|
+
`.vitepress/config.${ANY_MODULE}`,
|
|
516
|
+
`.vitepress/theme/index.${ANY_MODULE}`,
|
|
517
|
+
`**/*.data.${ANY_MODULE}`,
|
|
518
|
+
"**/*.vue",
|
|
519
|
+
],
|
|
520
|
+
moves: [],
|
|
521
|
+
},
|
|
475
522
|
{
|
|
476
523
|
name: "vitest",
|
|
477
524
|
package: "vitest",
|
|
@@ -493,5 +540,6 @@ export const CONVENTION_ROWS = [
|
|
|
493
540
|
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
494
541
|
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
495
542
|
],
|
|
543
|
+
testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
|
|
496
544
|
},
|
|
497
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) {
|
|
@@ -602,6 +631,50 @@ function helperUses(host, parse, dir, manifest, readings) {
|
|
|
602
631
|
readingFiles(host, parse, dir, reading.files).some(({ file }) => valuesAt(file, reading.property, reading.call).some((held) => holdsValue(held, reading.value))))
|
|
603
632
|
.map((reading) => reading.package);
|
|
604
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* The dependencies one manifest declares that an applied row's package keys use: each
|
|
636
|
+
* whose name a string under a member of a key's name equals, or begins followed by `/` or
|
|
637
|
+
* by `:` and a name. A member's own name is no such string.
|
|
638
|
+
*/
|
|
639
|
+
function packageKeyUses(host, parse, dir, manifest, keys) {
|
|
640
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
641
|
+
const held = isRecord(manifest) ? manifest[section] : undefined;
|
|
642
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
643
|
+
});
|
|
644
|
+
const strings = [];
|
|
645
|
+
const collect = (node) => {
|
|
646
|
+
const text = literalText(node);
|
|
647
|
+
if (text !== undefined) {
|
|
648
|
+
strings.push(text);
|
|
649
|
+
}
|
|
650
|
+
else if (isPropertyAssignment(node)) {
|
|
651
|
+
collect(node.initializer);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
node.forEachChild(collect);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
for (const { key, files } of keys) {
|
|
658
|
+
const visit = (node) => {
|
|
659
|
+
if (isPropertyAssignment(node) && keyOf(node) === key) {
|
|
660
|
+
collect(node.initializer);
|
|
661
|
+
}
|
|
662
|
+
node.forEachChild(visit);
|
|
663
|
+
};
|
|
664
|
+
for (const { file } of readingFiles(host, parse, dir, files)) {
|
|
665
|
+
const values = declaredValues(file);
|
|
666
|
+
for (const one of defaultExports(file)) {
|
|
667
|
+
const object = objectOf(one, values, true);
|
|
668
|
+
if (object !== undefined) {
|
|
669
|
+
visit(object);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return declared.filter((name) => strings.some((text) => text === name ||
|
|
675
|
+
text.startsWith(`${name}/`) ||
|
|
676
|
+
(text.startsWith(`${name}:`) && text.length > name.length + 1)));
|
|
677
|
+
}
|
|
605
678
|
/** Two strings ordered bytewise. */
|
|
606
679
|
function compare(a, b) {
|
|
607
680
|
if (a === b) {
|
|
@@ -620,6 +693,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
620
693
|
const failures = [];
|
|
621
694
|
const uses = new Set();
|
|
622
695
|
const selected = new Map();
|
|
696
|
+
const testFiles = new Set();
|
|
623
697
|
for (const manifest of manifests) {
|
|
624
698
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
625
699
|
const versions = new Map();
|
|
@@ -680,6 +754,10 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
680
754
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
681
755
|
uses.add(name);
|
|
682
756
|
}
|
|
757
|
+
const keys = row.packageKeys ?? [];
|
|
758
|
+
for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
|
|
759
|
+
uses.add(name);
|
|
760
|
+
}
|
|
683
761
|
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
684
762
|
uses.add(name);
|
|
685
763
|
}
|
|
@@ -687,6 +765,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
687
765
|
uses.add(name);
|
|
688
766
|
}
|
|
689
767
|
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
768
|
+
testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
|
|
690
769
|
const excludes = [
|
|
691
770
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
692
771
|
].map(globExpression);
|
|
@@ -714,6 +793,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
714
793
|
failures,
|
|
715
794
|
uses: [...uses].sort(compare),
|
|
716
795
|
selected,
|
|
796
|
+
testFiles,
|
|
717
797
|
};
|
|
718
798
|
}
|
|
719
799
|
/**
|
package/dist/src/dependencies.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dependencies the target's manifest and its workspace members' manifests declare, and
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* file the specifier resolves to; a compiler configuration needs one it names, and a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The dependencies the target's manifest and its workspace members' manifests declare, and what the
|
|
3
|
+
* projects' own files need from outside the target. A file needs a package it imports, re-exports
|
|
4
|
+
* from, augments, references the types of or asks the runtime to resolve, by name or through the
|
|
5
|
+
* file the specifier resolves to; a compiler configuration needs one it names, and a configuration
|
|
6
|
+
* file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
|
|
7
|
+
* itself is that dependency's need, not the target's.
|
|
8
8
|
*/
|
|
9
|
-
import { isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
9
|
+
import { isCallExpression, isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNamespaceExport, isPropertyAccessExpression, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
10
10
|
import { configurationFiles, declaredIn as declaredBeside, dependenciesNamed, moduleStrings, } from "./configuration-files.js";
|
|
11
11
|
import { nodeKey, packageScope } from "./inventory.js";
|
|
12
|
+
import { scriptTokens } from "./manifest.js";
|
|
12
13
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
|
|
13
14
|
import { UNANSWERED } from "./query.js";
|
|
14
15
|
import { sourceFilesOf } from "./source-files.js";
|
|
16
|
+
import { workflowTokens } from "./workflow-steps.js";
|
|
15
17
|
const MANIFEST = "package.json";
|
|
16
18
|
const MODULES_DIR = "node_modules";
|
|
17
19
|
const MODULES_SEGMENT = `/${MODULES_DIR}/`;
|
|
@@ -29,7 +31,7 @@ export function configurationNeeds(packages) {
|
|
|
29
31
|
return {
|
|
30
32
|
packages: new Set(packages),
|
|
31
33
|
uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
|
|
32
|
-
unanswered:
|
|
34
|
+
unanswered: [],
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
function isRecord(value) {
|
|
@@ -219,9 +221,11 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
|
|
|
219
221
|
}
|
|
220
222
|
};
|
|
221
223
|
use([PROJECT_OWNER], packages);
|
|
222
|
-
|
|
224
|
+
const unanswered = [];
|
|
223
225
|
for (const file of project.ownSourceFiles()) {
|
|
224
|
-
|
|
226
|
+
if (fileNeeds(project, held, file, programPackages, use)) {
|
|
227
|
+
unanswered.push(file.fileName);
|
|
228
|
+
}
|
|
225
229
|
}
|
|
226
230
|
const beside = new Map();
|
|
227
231
|
const declaredAt = (path) => {
|
|
@@ -240,16 +244,20 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
|
|
|
240
244
|
use(ownersOf(file, held, node), dependenciesNamed(text, names));
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
|
-
for (const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
|
247
253
|
}
|
|
248
254
|
}
|
|
249
|
-
for (const { file,
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
}
|
|
253
261
|
}
|
|
254
262
|
}
|
|
255
263
|
return { packages, uses, unanswered };
|
|
@@ -365,8 +373,40 @@ function fileNeeds(project, held, file, programPackages, use) {
|
|
|
365
373
|
for (const reference of file.typeReferenceDirectives) {
|
|
366
374
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
367
375
|
}
|
|
376
|
+
for (const specifier of resolvedSpecifiers(file)) {
|
|
377
|
+
const named = packageOfSpecifier(specifier.text);
|
|
378
|
+
if (named !== undefined) {
|
|
379
|
+
use(ownersOf(file, held, specifier), [named]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
368
382
|
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
369
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
|
|
386
|
+
* or `require.resolve`, each of which needs the package it names installed.
|
|
387
|
+
*/
|
|
388
|
+
function resolvedSpecifiers(file) {
|
|
389
|
+
const found = [];
|
|
390
|
+
if (!file.text.includes(".resolve(")) {
|
|
391
|
+
return found;
|
|
392
|
+
}
|
|
393
|
+
const visit = (node) => {
|
|
394
|
+
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
395
|
+
const callee = node.expression;
|
|
396
|
+
const [argument] = node.arguments;
|
|
397
|
+
const receiver = callee.expression;
|
|
398
|
+
const resolving = callee.name.text === "resolve" &&
|
|
399
|
+
((isMetaProperty(receiver) && receiver.name.text === "meta") ||
|
|
400
|
+
(isIdentifier(receiver) && receiver.text === "require"));
|
|
401
|
+
if (resolving && argument !== undefined && isLiteralText(argument)) {
|
|
402
|
+
found.push(argument);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
node.forEachChild(visit);
|
|
406
|
+
};
|
|
407
|
+
visit(file);
|
|
408
|
+
return found;
|
|
409
|
+
}
|
|
370
410
|
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
371
411
|
function valueEnd(text, at) {
|
|
372
412
|
const first = text.charAt(at);
|
|
@@ -455,20 +495,16 @@ function installedOf(host, root, name) {
|
|
|
455
495
|
for (let dir = root;; dir = dirnamePath(dir)) {
|
|
456
496
|
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
457
497
|
if (read !== undefined && isRecord(read.value)) {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
const peers = manifest["peerDependencies"];
|
|
461
|
-
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
462
|
-
? manifest["peerDependenciesMeta"]
|
|
463
|
-
: {};
|
|
498
|
+
const bin = read.value["bin"];
|
|
499
|
+
// A string `bin` names one command, the package's name without its scope.
|
|
464
500
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
501
|
+
commands: typeof bin === "string"
|
|
502
|
+
? bin === ""
|
|
503
|
+
? []
|
|
504
|
+
: [name.slice(name.indexOf("/") + 1)]
|
|
505
|
+
: isRecord(bin)
|
|
506
|
+
? Object.keys(bin)
|
|
507
|
+
: [],
|
|
472
508
|
};
|
|
473
509
|
}
|
|
474
510
|
if (dirnamePath(dir) === dir) {
|
|
@@ -501,37 +537,70 @@ function declaredIn(text, manifest, path, scope) {
|
|
|
501
537
|
}
|
|
502
538
|
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
503
539
|
}
|
|
540
|
+
/** The key one declared dependency is known by: its manifest's path and its name. */
|
|
541
|
+
export function declarationKey(dependency) {
|
|
542
|
+
return `${dependency.position.path}\u0000${dependency.name}`;
|
|
543
|
+
}
|
|
544
|
+
/** Every token of every value of one manifest's `scripts` member. */
|
|
545
|
+
function scriptTokensOf(manifest) {
|
|
546
|
+
const scripts = manifest["scripts"];
|
|
547
|
+
const found = new Set();
|
|
548
|
+
if (isRecord(scripts)) {
|
|
549
|
+
for (const command of Object.values(scripts)) {
|
|
550
|
+
if (typeof command === "string") {
|
|
551
|
+
scriptTokens(command).forEach((token) => found.add(token));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return found;
|
|
556
|
+
}
|
|
557
|
+
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
558
|
+
function isInside(path, dir) {
|
|
559
|
+
return dir === "" || path.startsWith(`${dir}/`);
|
|
560
|
+
}
|
|
504
561
|
/**
|
|
505
562
|
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
506
563
|
* `members` below the target root, read with the position of every dependency each
|
|
507
|
-
* declares, the
|
|
508
|
-
*
|
|
509
|
-
* program cannot read declares
|
|
564
|
+
* declares, what the projects and the commands its scripts and workflow steps run need,
|
|
565
|
+
* and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
|
|
566
|
+
* in configurations the run could not build. A manifest the program cannot read declares
|
|
567
|
+
* nothing.
|
|
510
568
|
*/
|
|
511
|
-
export function dependenciesOf(host, targetRoot, perProject, candidates, members = []) {
|
|
569
|
+
export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
|
|
512
570
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
513
571
|
const scopeOf = packageScope(host, targetRoot);
|
|
514
572
|
const manifests = new Map([[root, MANIFEST]]);
|
|
515
|
-
|
|
573
|
+
const unbuilt = new Set();
|
|
574
|
+
for (const dir of [...members, ...unbuiltMembers]) {
|
|
516
575
|
const below = relativePath(root, dir);
|
|
517
576
|
if (below !== undefined && !manifests.has(dir)) {
|
|
518
577
|
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
519
578
|
}
|
|
520
579
|
}
|
|
580
|
+
for (const dir of unbuiltMembers) {
|
|
581
|
+
const path = manifests.get(dir);
|
|
582
|
+
if (path !== undefined && dir !== root) {
|
|
583
|
+
unbuilt.add(path);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
521
587
|
const declared = [];
|
|
522
|
-
const
|
|
588
|
+
const ran = new Set();
|
|
523
589
|
for (const [dir, path] of manifests) {
|
|
524
590
|
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
525
591
|
if (read === undefined || !isRecord(read.value)) {
|
|
526
592
|
continue;
|
|
527
593
|
}
|
|
594
|
+
const tokens = scriptTokensOf(read.value);
|
|
528
595
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
529
596
|
declared.push(dependency);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
597
|
+
const found = installedOf(host, dir, dependency.name);
|
|
598
|
+
if (found === undefined) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
|
|
602
|
+
if (found.commands.some(runs)) {
|
|
603
|
+
ran.add(declarationKey(dependency));
|
|
535
604
|
}
|
|
536
605
|
}
|
|
537
606
|
}
|
|
@@ -540,9 +609,15 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
540
609
|
const needed = new Set();
|
|
541
610
|
const uses = new Map();
|
|
542
611
|
for (const project of perProject) {
|
|
543
|
-
// A specifier the checker left unanswered may resolve to any
|
|
544
|
-
// and what the configuration needs is never deleted
|
|
545
|
-
const
|
|
612
|
+
// A specifier the checker left unanswered may resolve to any dependency a manifest at
|
|
613
|
+
// or above its file declares, and what the configuration needs is never deleted.
|
|
614
|
+
const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
|
|
615
|
+
const unknown = declared
|
|
616
|
+
.filter((dependency) => {
|
|
617
|
+
const dir = dirnamePath(dependency.position.path);
|
|
618
|
+
return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
|
|
619
|
+
})
|
|
620
|
+
.map((dependency) => dependency.name);
|
|
546
621
|
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
|
|
547
622
|
for (const [id, used] of [...project.uses, ...held]) {
|
|
548
623
|
const merged = uses.get(id) ?? new Set();
|
|
@@ -563,5 +638,5 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
563
638
|
lastUses.set(id, sole.sort((a, b) => (a < b ? -1 : 1)));
|
|
564
639
|
}
|
|
565
640
|
}
|
|
566
|
-
return { declared,
|
|
641
|
+
return { declared, needed, ran, unbuilt, lastUses };
|
|
567
642
|
}
|
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";
|