@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.1
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 +39 -16
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +27 -3
- package/dist/src/convention-rows.js +42 -0
- package/dist/src/conventions.js +48 -0
- package/dist/src/dependencies.js +109 -38
- package/dist/src/findings/completion.js +6 -7
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +371 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +60 -21
- package/dist/src/roots.js +96 -6
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +1 -0
- 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 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -17
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +36 -4
- package/src/convention-rows.ts +57 -0
- package/src/conventions.ts +59 -0
- package/src/dependencies.ts +134 -46
- package/src/finding.ts +2 -0
- package/src/findings/completion.ts +7 -7
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/html-entries.ts +33 -15
- package/src/indexed-access.ts +468 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/kinds.ts +7 -6
- package/src/references.ts +86 -27
- package/src/roots.ts +129 -6
- package/src/session.ts +7 -1
- package/src/source-files.ts +3 -0
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/test-joins.ts +120 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +1 -1
package/dist/src/analysis.js
CHANGED
|
@@ -32,7 +32,7 @@ import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
|
|
|
32
32
|
import { inventory } from "./inventory.js";
|
|
33
33
|
import { packageEntries, readManifest } from "./manifest.js";
|
|
34
34
|
import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
|
|
35
|
-
import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
35
|
+
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
|
|
36
36
|
import { byPosition, isComponentFile } from "./position.js";
|
|
37
37
|
import { SetupError, setupLine } from "./setup-failure.js";
|
|
38
38
|
import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
|
|
@@ -41,6 +41,7 @@ import { references, testFileRulesOf } from "./references.js";
|
|
|
41
41
|
import { roots, unmatchedEverywhere } from "./roots.js";
|
|
42
42
|
import { serializationContract } from "./serialization-contract.js";
|
|
43
43
|
import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
|
|
44
|
+
import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
|
|
44
45
|
import { supportReferences, testSupportFiles } from "./test-support.js";
|
|
45
46
|
import { diagnosticsOf, ownedAs, runSession } from "./session.js";
|
|
46
47
|
import { accessorsOf, storesOf } from "./stores.js";
|
|
@@ -119,6 +120,7 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
119
120
|
testFiles: config.ts.testFiles,
|
|
120
121
|
parse: (fileName, text) => engine.parseSourceFile(fileName, text),
|
|
121
122
|
publishedAPI: config.targetKind === "library",
|
|
123
|
+
publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
|
|
122
124
|
};
|
|
123
125
|
const notBuilt = [...discovered.notBuilt];
|
|
124
126
|
const dropped = (configFile, error) => {
|
|
@@ -144,6 +146,10 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
144
146
|
unbuildable.push(...met);
|
|
145
147
|
}
|
|
146
148
|
};
|
|
149
|
+
const testPatterns = config.ts.testFiles.map(globExpression);
|
|
150
|
+
const joins = discovered.derived
|
|
151
|
+
? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
|
|
152
|
+
: new Map();
|
|
147
153
|
const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
|
|
148
154
|
const consumer = consumerOf.get(opened.configFile);
|
|
149
155
|
const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
|
|
@@ -243,9 +249,10 @@ function readProjects(engine, host, scope, config, stage) {
|
|
|
243
249
|
heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
|
|
244
250
|
}
|
|
245
251
|
return stage(project, { configuration, read });
|
|
246
|
-
}, discovered.derived ? "record" : "refuse",
|
|
247
|
-
|
|
248
|
-
|
|
252
|
+
}, discovered.derived ? "record" : "refuse", joins.size === 0
|
|
253
|
+
? workspace?.programs
|
|
254
|
+
: withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins), () => {
|
|
255
|
+
outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), options.parse);
|
|
249
256
|
});
|
|
250
257
|
for (const configFile of session.unopened) {
|
|
251
258
|
if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
|
|
@@ -638,23 +645,38 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
|
|
|
638
645
|
: { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
|
|
639
646
|
}
|
|
640
647
|
/**
|
|
641
|
-
* The workspace members whose
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
648
|
+
* The workspace members whose dependencies the run judges: `read` holds those a built
|
|
649
|
+
* project holds a file below, `unbuilt` the others whose own directory, the nearest
|
|
650
|
+
* manifest's above the configuration, holds a configuration the run could not build. A
|
|
651
|
+
* member no configuration holds a file of is in neither.
|
|
645
652
|
*/
|
|
646
|
-
function
|
|
653
|
+
function judgedMembers(host, targetRoot, read) {
|
|
647
654
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
648
655
|
const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
|
|
649
|
-
const
|
|
650
|
-
|
|
656
|
+
const owners = new Set(read.notBuilt.map((one) => nearestManifestDir(host, root, one.configFile)));
|
|
657
|
+
const judged = { read: [], unbuilt: [] };
|
|
658
|
+
for (const dir of read.memberDirs) {
|
|
651
659
|
const below = relativePath(root, dir);
|
|
652
660
|
if (below === undefined) {
|
|
653
|
-
|
|
661
|
+
continue;
|
|
654
662
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
663
|
+
if ([...held].some((path) => path.startsWith(`${below}/`))) {
|
|
664
|
+
judged.read.push(dir);
|
|
665
|
+
}
|
|
666
|
+
else if (owners.has(dir)) {
|
|
667
|
+
judged.unbuilt.push(dir);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return judged;
|
|
671
|
+
}
|
|
672
|
+
/** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
|
|
673
|
+
function nearestManifestDir(host, root, file) {
|
|
674
|
+
for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
|
|
675
|
+
if (host.kindOf(joinPath(dir, "package.json")) === "file") {
|
|
676
|
+
return dir;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return root;
|
|
658
680
|
}
|
|
659
681
|
/**
|
|
660
682
|
* The input every emitter reads over one sweep of a read run: the facts beside the sweep
|
|
@@ -664,6 +686,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
664
686
|
const targetRoot = scope.target.path;
|
|
665
687
|
const swept = sweptOf(read, input);
|
|
666
688
|
const { projects } = read;
|
|
689
|
+
const members = judgedMembers(host, targetRoot, read);
|
|
667
690
|
return {
|
|
668
691
|
config,
|
|
669
692
|
swept,
|
|
@@ -672,7 +695,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
|
|
|
672
695
|
references: one.configured.references,
|
|
673
696
|
accessors: one.extra.accessors,
|
|
674
697
|
})), swept.exempt, input.mode),
|
|
675
|
-
dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id),
|
|
698
|
+
dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt),
|
|
676
699
|
implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
|
|
677
700
|
files: {
|
|
678
701
|
tree: sourceTree(host, targetRoot),
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "5.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
};
|
|
@@ -11,7 +11,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. */
|
|
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
|
}
|
|
@@ -88,6 +92,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
88
92
|
const writes = [];
|
|
89
93
|
batches += batchesOf(literals.length);
|
|
90
94
|
const contexts = queries.contextualTypes(literals.map((one) => one.literal), cap);
|
|
95
|
+
// A computed name that is not literal text names the property the literal declares
|
|
96
|
+
// under the checker's own key, which is how a type declares a member a unique symbol
|
|
97
|
+
// keys: asked once, for the literals that have a contextual type.
|
|
98
|
+
const computed = literals.flatMap(({ literal }, index) => contexts[index] === undefined || !isAnswered(contexts[index])
|
|
99
|
+
? []
|
|
100
|
+
: literal.properties.flatMap((property) => {
|
|
101
|
+
const name = propertyName(property);
|
|
102
|
+
return name !== undefined && name.text === undefined ? [name.node] : [];
|
|
103
|
+
}));
|
|
104
|
+
batches += batchesOf(computed.length);
|
|
105
|
+
const keys = new Map();
|
|
106
|
+
(computed.length === 0
|
|
107
|
+
? []
|
|
108
|
+
: project.symbolsAt(computed.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
|
|
109
|
+
const node = computed[index];
|
|
110
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
111
|
+
keys.set(node, symbol.name);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
91
114
|
literals.forEach(({ literal, from }, index) => {
|
|
92
115
|
const context = contexts[index];
|
|
93
116
|
if (context === undefined || !isAnswered(context)) {
|
|
@@ -96,10 +119,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
|
|
|
96
119
|
const tablesOf = partsOf(context).map(membersOf);
|
|
97
120
|
for (const property of literal.properties) {
|
|
98
121
|
const name = propertyName(property);
|
|
99
|
-
|
|
122
|
+
const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
|
|
123
|
+
if (name === undefined || text === undefined) {
|
|
100
124
|
continue;
|
|
101
125
|
}
|
|
102
|
-
const ids = [...new Set(tablesOf.flatMap((table) => table.get(
|
|
126
|
+
const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
|
|
103
127
|
if (ids.length > 0) {
|
|
104
128
|
writes.push({ at: name.node, from, ids });
|
|
105
129
|
}
|
|
@@ -39,8 +39,38 @@ 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
|
+
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
43
|
+
const ANGULAR_ENTRIES = [
|
|
44
|
+
"src/{main,main.server,server,polyfills,test}.ts",
|
|
45
|
+
"server.ts",
|
|
46
|
+
"projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
|
|
47
|
+
"projects/*/server.ts",
|
|
48
|
+
];
|
|
49
|
+
/** The `angular.json` members whose strings name the packages a build loads. */
|
|
50
|
+
const ANGULAR_PACKAGE_KEYS = [
|
|
51
|
+
"builder",
|
|
52
|
+
"polyfills",
|
|
53
|
+
"styles",
|
|
54
|
+
"scripts",
|
|
55
|
+
].map((key) => ({ key, files: ["angular.json"] }));
|
|
42
56
|
/** Every row, ordered by name and then by range. */
|
|
43
57
|
export const CONVENTION_ROWS = [
|
|
58
|
+
{
|
|
59
|
+
name: "angular",
|
|
60
|
+
package: "@angular/build",
|
|
61
|
+
range: ">=17.0.0 <22.0.0",
|
|
62
|
+
entries: ANGULAR_ENTRIES,
|
|
63
|
+
moves: [],
|
|
64
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "angular-devkit",
|
|
68
|
+
package: "@angular-devkit/build-angular",
|
|
69
|
+
range: ">=12.0.0 <22.0.0",
|
|
70
|
+
entries: ANGULAR_ENTRIES,
|
|
71
|
+
moves: [],
|
|
72
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
73
|
+
},
|
|
44
74
|
{
|
|
45
75
|
name: "astro",
|
|
46
76
|
package: "astro",
|
|
@@ -472,6 +502,18 @@ export const CONVENTION_ROWS = [
|
|
|
472
502
|
},
|
|
473
503
|
],
|
|
474
504
|
},
|
|
505
|
+
{
|
|
506
|
+
name: "vitepress",
|
|
507
|
+
package: "vitepress",
|
|
508
|
+
range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
|
|
509
|
+
entries: [
|
|
510
|
+
`.vitepress/config.${ANY_MODULE}`,
|
|
511
|
+
`.vitepress/theme/index.${ANY_MODULE}`,
|
|
512
|
+
`**/*.data.${ANY_MODULE}`,
|
|
513
|
+
"**/*.vue",
|
|
514
|
+
],
|
|
515
|
+
moves: [],
|
|
516
|
+
},
|
|
475
517
|
{
|
|
476
518
|
name: "vitest",
|
|
477
519
|
package: "vitest",
|
package/dist/src/conventions.js
CHANGED
|
@@ -602,6 +602,50 @@ function helperUses(host, parse, dir, manifest, readings) {
|
|
|
602
602
|
readingFiles(host, parse, dir, reading.files).some(({ file }) => valuesAt(file, reading.property, reading.call).some((held) => holdsValue(held, reading.value))))
|
|
603
603
|
.map((reading) => reading.package);
|
|
604
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* The dependencies one manifest declares that an applied row's package keys use: each
|
|
607
|
+
* whose name a string under a member of a key's name equals, or begins followed by `/` or
|
|
608
|
+
* by `:` and a name. A member's own name is no such string.
|
|
609
|
+
*/
|
|
610
|
+
function packageKeyUses(host, parse, dir, manifest, keys) {
|
|
611
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
612
|
+
const held = isRecord(manifest) ? manifest[section] : undefined;
|
|
613
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
614
|
+
});
|
|
615
|
+
const strings = [];
|
|
616
|
+
const collect = (node) => {
|
|
617
|
+
const text = literalText(node);
|
|
618
|
+
if (text !== undefined) {
|
|
619
|
+
strings.push(text);
|
|
620
|
+
}
|
|
621
|
+
else if (isPropertyAssignment(node)) {
|
|
622
|
+
collect(node.initializer);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
node.forEachChild(collect);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
for (const { key, files } of keys) {
|
|
629
|
+
const visit = (node) => {
|
|
630
|
+
if (isPropertyAssignment(node) && keyOf(node) === key) {
|
|
631
|
+
collect(node.initializer);
|
|
632
|
+
}
|
|
633
|
+
node.forEachChild(visit);
|
|
634
|
+
};
|
|
635
|
+
for (const { file } of readingFiles(host, parse, dir, files)) {
|
|
636
|
+
const values = declaredValues(file);
|
|
637
|
+
for (const one of defaultExports(file)) {
|
|
638
|
+
const object = objectOf(one, values, true);
|
|
639
|
+
if (object !== undefined) {
|
|
640
|
+
visit(object);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return declared.filter((name) => strings.some((text) => text === name ||
|
|
646
|
+
text.startsWith(`${name}/`) ||
|
|
647
|
+
(text.startsWith(`${name}:`) && text.length > name.length + 1)));
|
|
648
|
+
}
|
|
605
649
|
/** Two strings ordered bytewise. */
|
|
606
650
|
function compare(a, b) {
|
|
607
651
|
if (a === b) {
|
|
@@ -680,6 +724,10 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
|
|
|
680
724
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
681
725
|
uses.add(name);
|
|
682
726
|
}
|
|
727
|
+
const keys = row.packageKeys ?? [];
|
|
728
|
+
for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
|
|
729
|
+
uses.add(name);
|
|
730
|
+
}
|
|
683
731
|
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
684
732
|
uses.add(name);
|
|
685
733
|
}
|
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) => {
|
|
@@ -365,8 +369,40 @@ function fileNeeds(project, held, file, programPackages, use) {
|
|
|
365
369
|
for (const reference of file.typeReferenceDirectives) {
|
|
366
370
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
367
371
|
}
|
|
372
|
+
for (const specifier of resolvedSpecifiers(file)) {
|
|
373
|
+
const named = packageOfSpecifier(specifier.text);
|
|
374
|
+
if (named !== undefined) {
|
|
375
|
+
use(ownersOf(file, held, specifier), [named]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
368
378
|
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
369
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
|
|
382
|
+
* or `require.resolve`, each of which needs the package it names installed.
|
|
383
|
+
*/
|
|
384
|
+
function resolvedSpecifiers(file) {
|
|
385
|
+
const found = [];
|
|
386
|
+
if (!file.text.includes(".resolve(")) {
|
|
387
|
+
return found;
|
|
388
|
+
}
|
|
389
|
+
const visit = (node) => {
|
|
390
|
+
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
391
|
+
const callee = node.expression;
|
|
392
|
+
const [argument] = node.arguments;
|
|
393
|
+
const receiver = callee.expression;
|
|
394
|
+
const resolving = callee.name.text === "resolve" &&
|
|
395
|
+
((isMetaProperty(receiver) && receiver.name.text === "meta") ||
|
|
396
|
+
(isIdentifier(receiver) && receiver.text === "require"));
|
|
397
|
+
if (resolving && argument !== undefined && isLiteralText(argument)) {
|
|
398
|
+
found.push(argument);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
node.forEachChild(visit);
|
|
402
|
+
};
|
|
403
|
+
visit(file);
|
|
404
|
+
return found;
|
|
405
|
+
}
|
|
370
406
|
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
371
407
|
function valueEnd(text, at) {
|
|
372
408
|
const first = text.charAt(at);
|
|
@@ -455,20 +491,16 @@ function installedOf(host, root, name) {
|
|
|
455
491
|
for (let dir = root;; dir = dirnamePath(dir)) {
|
|
456
492
|
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
457
493
|
if (read !== undefined && isRecord(read.value)) {
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
const peers = manifest["peerDependencies"];
|
|
461
|
-
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
462
|
-
? manifest["peerDependenciesMeta"]
|
|
463
|
-
: {};
|
|
494
|
+
const bin = read.value["bin"];
|
|
495
|
+
// A string `bin` names one command, the package's name without its scope.
|
|
464
496
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
497
|
+
commands: typeof bin === "string"
|
|
498
|
+
? bin === ""
|
|
499
|
+
? []
|
|
500
|
+
: [name.slice(name.indexOf("/") + 1)]
|
|
501
|
+
: isRecord(bin)
|
|
502
|
+
? Object.keys(bin)
|
|
503
|
+
: [],
|
|
472
504
|
};
|
|
473
505
|
}
|
|
474
506
|
if (dirnamePath(dir) === dir) {
|
|
@@ -501,37 +533,70 @@ function declaredIn(text, manifest, path, scope) {
|
|
|
501
533
|
}
|
|
502
534
|
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
503
535
|
}
|
|
536
|
+
/** The key one declared dependency is known by: its manifest's path and its name. */
|
|
537
|
+
export function declarationKey(dependency) {
|
|
538
|
+
return `${dependency.position.path}\u0000${dependency.name}`;
|
|
539
|
+
}
|
|
540
|
+
/** Every token of every value of one manifest's `scripts` member. */
|
|
541
|
+
function scriptTokensOf(manifest) {
|
|
542
|
+
const scripts = manifest["scripts"];
|
|
543
|
+
const found = new Set();
|
|
544
|
+
if (isRecord(scripts)) {
|
|
545
|
+
for (const command of Object.values(scripts)) {
|
|
546
|
+
if (typeof command === "string") {
|
|
547
|
+
scriptTokens(command).forEach((token) => found.add(token));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
554
|
+
function isInside(path, dir) {
|
|
555
|
+
return dir === "" || path.startsWith(`${dir}/`);
|
|
556
|
+
}
|
|
504
557
|
/**
|
|
505
558
|
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
506
559
|
* `members` below the target root, read with the position of every dependency each
|
|
507
|
-
* declares, the
|
|
508
|
-
*
|
|
509
|
-
* program cannot read declares
|
|
560
|
+
* declares, what the projects and the commands its scripts and workflow steps run need,
|
|
561
|
+
* and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
|
|
562
|
+
* in configurations the run could not build. A manifest the program cannot read declares
|
|
563
|
+
* nothing.
|
|
510
564
|
*/
|
|
511
|
-
export function dependenciesOf(host, targetRoot, perProject, candidates, members = []) {
|
|
565
|
+
export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
|
|
512
566
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
513
567
|
const scopeOf = packageScope(host, targetRoot);
|
|
514
568
|
const manifests = new Map([[root, MANIFEST]]);
|
|
515
|
-
|
|
569
|
+
const unbuilt = new Set();
|
|
570
|
+
for (const dir of [...members, ...unbuiltMembers]) {
|
|
516
571
|
const below = relativePath(root, dir);
|
|
517
572
|
if (below !== undefined && !manifests.has(dir)) {
|
|
518
573
|
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
519
574
|
}
|
|
520
575
|
}
|
|
576
|
+
for (const dir of unbuiltMembers) {
|
|
577
|
+
const path = manifests.get(dir);
|
|
578
|
+
if (path !== undefined && dir !== root) {
|
|
579
|
+
unbuilt.add(path);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
521
583
|
const declared = [];
|
|
522
|
-
const
|
|
584
|
+
const ran = new Set();
|
|
523
585
|
for (const [dir, path] of manifests) {
|
|
524
586
|
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
525
587
|
if (read === undefined || !isRecord(read.value)) {
|
|
526
588
|
continue;
|
|
527
589
|
}
|
|
590
|
+
const tokens = scriptTokensOf(read.value);
|
|
528
591
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
529
592
|
declared.push(dependency);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
593
|
+
const found = installedOf(host, dir, dependency.name);
|
|
594
|
+
if (found === undefined) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
|
|
598
|
+
if (found.commands.some(runs)) {
|
|
599
|
+
ran.add(declarationKey(dependency));
|
|
535
600
|
}
|
|
536
601
|
}
|
|
537
602
|
}
|
|
@@ -540,9 +605,15 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
540
605
|
const needed = new Set();
|
|
541
606
|
const uses = new Map();
|
|
542
607
|
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
|
|
608
|
+
// A specifier the checker left unanswered may resolve to any dependency a manifest at
|
|
609
|
+
// or above its file declares, and what the configuration needs is never deleted.
|
|
610
|
+
const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
|
|
611
|
+
const unknown = declared
|
|
612
|
+
.filter((dependency) => {
|
|
613
|
+
const dir = dirnamePath(dependency.position.path);
|
|
614
|
+
return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
|
|
615
|
+
})
|
|
616
|
+
.map((dependency) => dependency.name);
|
|
546
617
|
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
|
|
547
618
|
for (const [id, used] of [...project.uses, ...held]) {
|
|
548
619
|
const merged = uses.get(id) ?? new Set();
|
|
@@ -563,5 +634,5 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
563
634
|
lastUses.set(id, sole.sort((a, b) => (a < b ? -1 : 1)));
|
|
564
635
|
}
|
|
565
636
|
}
|
|
566
|
-
return { declared,
|
|
637
|
+
return { declared, needed, ran, unbuilt, lastUses };
|
|
567
638
|
}
|
|
@@ -108,12 +108,11 @@ function findingComponents(swept, cascade) {
|
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
110
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
111
|
-
* candidate's relation, counts, configurations and component; a live one carries no
|
|
112
|
-
*
|
|
113
|
-
* holds in every configuration. A subject in no dead component gets one
|
|
114
|
-
* numbered past the computed ones in finding order
|
|
115
|
-
*
|
|
116
|
-
* row is read from, the shipped one unless a caller supplies its own.
|
|
111
|
+
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
112
|
+
* holds where it is declared; a file or a manifest row carries the class its emitter states,
|
|
113
|
+
* `certain` by default, and holds in every configuration. A subject in no dead component gets one
|
|
114
|
+
* of its own, numbered past the computed ones in finding order so no two families mint one, whose
|
|
115
|
+
* deletion removes no line. `kinds` defaults to the shipped vocabulary.
|
|
117
116
|
*/
|
|
118
117
|
export function completed(input, findings, kinds = KINDS) {
|
|
119
118
|
const { swept } = input;
|
|
@@ -172,7 +171,7 @@ export function completed(input, findings, kinds = KINDS) {
|
|
|
172
171
|
const counts = candidate === undefined
|
|
173
172
|
? union.made[at]
|
|
174
173
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
175
|
-
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
174
|
+
const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
176
175
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
177
176
|
return {
|
|
178
177
|
code: finding.code,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { declarationKey } from "../dependencies.js";
|
|
1
2
|
import { renderRef } from "../ref.js";
|
|
2
3
|
const UNUSED_DEPENDENCY = "DS1601";
|
|
3
4
|
const MESSAGES = {
|
|
@@ -5,38 +6,14 @@ const MESSAGES = {
|
|
|
5
6
|
"dev-dependency": "no import in the target needs the declared development dependency",
|
|
6
7
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
7
8
|
};
|
|
8
|
-
/**
|
|
9
|
-
* The dependencies the target needs: every package the projects' uses name, every
|
|
10
|
-
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
11
|
-
* runs outside anything the analysis reads, and every peer a needed package
|
|
12
|
-
* requires, which the target satisfies by declaring it.
|
|
13
|
-
*/
|
|
14
|
-
function neededBy(dependencies) {
|
|
15
|
-
const needed = new Set(dependencies.needed);
|
|
16
|
-
for (const [name, installed] of dependencies.installed) {
|
|
17
|
-
if (installed.command) {
|
|
18
|
-
needed.add(name);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
const pending = [...needed];
|
|
22
|
-
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
23
|
-
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
24
|
-
if (!needed.has(peer)) {
|
|
25
|
-
needed.add(peer);
|
|
26
|
-
pending.push(peer);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return needed;
|
|
31
|
-
}
|
|
32
9
|
/**
|
|
33
10
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
34
11
|
* that the target does not need, at the position of its key.
|
|
35
12
|
*/
|
|
36
13
|
function unusedDependencies(dependencies) {
|
|
37
|
-
const needed = neededBy(dependencies);
|
|
38
14
|
return dependencies.declared
|
|
39
|
-
.filter((dependency) => !needed.has(dependency.name)
|
|
15
|
+
.filter((dependency) => !dependencies.needed.has(dependency.name) &&
|
|
16
|
+
!dependencies.ran.has(declarationKey(dependency)))
|
|
40
17
|
.map((dependency) => ({
|
|
41
18
|
code: UNUSED_DEPENDENCY,
|
|
42
19
|
position: dependency.position,
|
|
@@ -52,6 +29,9 @@ function unusedDependencies(dependencies) {
|
|
|
52
29
|
},
|
|
53
30
|
message: MESSAGES[dependency.section],
|
|
54
31
|
details: { dependencyClass: dependency.section },
|
|
32
|
+
...(dependencies.unbuilt.has(dependency.position.path)
|
|
33
|
+
? { reachabilityClass: "possible" }
|
|
34
|
+
: {}),
|
|
55
35
|
}));
|
|
56
36
|
}
|
|
57
37
|
/** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */
|