@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
package/src/convention-rows.ts
CHANGED
|
@@ -45,6 +45,24 @@ export interface HelperReading extends MoveReading {
|
|
|
45
45
|
readonly package: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** One member of a configuration file whose strings name the packages its tool loads. */
|
|
49
|
+
export interface PackageKey {
|
|
50
|
+
/** The member's name, matched at any depth of the objects the row reads. */
|
|
51
|
+
readonly key: string;
|
|
52
|
+
/** Globs naming the configuration files, matched in the manifest's directory alone. */
|
|
53
|
+
readonly files: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One configuration property whose strings name files the test runner loads beside the tests. */
|
|
57
|
+
export interface TestFileReading extends MoveReading {
|
|
58
|
+
/**
|
|
59
|
+
* The properties that move the directory the strings are read against, the first one the
|
|
60
|
+
* file writes deciding, read against the manifest's directory, which stands where none is
|
|
61
|
+
* written. A value that is not one string literal leaves the file's strings unread.
|
|
62
|
+
*/
|
|
63
|
+
readonly roots: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/** One directory a row's globs name that a configuration property moves. */
|
|
49
67
|
export interface DirectoryMove {
|
|
50
68
|
/** The placeholder the row's globs and later moves write as `<id>`. */
|
|
@@ -83,6 +101,13 @@ export interface ConventionRow {
|
|
|
83
101
|
* string there names no file the tool runs or loads.
|
|
84
102
|
*/
|
|
85
103
|
readonly selections?: readonly MoveReading[];
|
|
104
|
+
/** The properties whose strings name a file the test runner loads beside the tests, a test file. */
|
|
105
|
+
readonly testFiles?: readonly TestFileReading[];
|
|
106
|
+
/**
|
|
107
|
+
* The members whose strings name a package the tool loads: the package itself, a
|
|
108
|
+
* subpath of it, or the package followed by `:` and a name.
|
|
109
|
+
*/
|
|
110
|
+
readonly packageKeys?: readonly PackageKey[];
|
|
86
111
|
/** The members of a `package.json` the tool reads its configuration from. */
|
|
87
112
|
readonly manifestKeys?: readonly string[];
|
|
88
113
|
/** The settings that make the tool's output import a runtime helper package. */
|
|
@@ -131,8 +156,46 @@ const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
|
|
|
131
156
|
const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
|
|
132
157
|
const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
|
|
133
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
|
|
161
|
+
* resolves `setupFiles` against that root (https://vitest.dev/config/setupfiles).
|
|
162
|
+
*/
|
|
163
|
+
const VITEST_ROOTS = ["test.root", "root"] as const;
|
|
164
|
+
|
|
165
|
+
/** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
|
|
166
|
+
const ANGULAR_ENTRIES = [
|
|
167
|
+
"src/{main,main.server,server,polyfills,test}.ts",
|
|
168
|
+
"server.ts",
|
|
169
|
+
"projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
|
|
170
|
+
"projects/*/server.ts",
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** The `angular.json` members whose strings name the packages a build loads. */
|
|
174
|
+
const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
|
|
175
|
+
"builder",
|
|
176
|
+
"polyfills",
|
|
177
|
+
"styles",
|
|
178
|
+
"scripts",
|
|
179
|
+
].map((key) => ({ key, files: ["angular.json"] }));
|
|
180
|
+
|
|
134
181
|
/** Every row, ordered by name and then by range. */
|
|
135
182
|
export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
183
|
+
{
|
|
184
|
+
name: "angular",
|
|
185
|
+
package: "@angular/build",
|
|
186
|
+
range: ">=17.0.0 <22.0.0",
|
|
187
|
+
entries: ANGULAR_ENTRIES,
|
|
188
|
+
moves: [],
|
|
189
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "angular-devkit",
|
|
193
|
+
package: "@angular-devkit/build-angular",
|
|
194
|
+
range: ">=12.0.0 <22.0.0",
|
|
195
|
+
entries: ANGULAR_ENTRIES,
|
|
196
|
+
moves: [],
|
|
197
|
+
packageKeys: ANGULAR_PACKAGE_KEYS,
|
|
198
|
+
},
|
|
136
199
|
{
|
|
137
200
|
name: "astro",
|
|
138
201
|
package: "astro",
|
|
@@ -564,6 +627,18 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
564
627
|
},
|
|
565
628
|
],
|
|
566
629
|
},
|
|
630
|
+
{
|
|
631
|
+
name: "vitepress",
|
|
632
|
+
package: "vitepress",
|
|
633
|
+
range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
|
|
634
|
+
entries: [
|
|
635
|
+
`.vitepress/config.${ANY_MODULE}`,
|
|
636
|
+
`.vitepress/theme/index.${ANY_MODULE}`,
|
|
637
|
+
`**/*.data.${ANY_MODULE}`,
|
|
638
|
+
"**/*.vue",
|
|
639
|
+
],
|
|
640
|
+
moves: [],
|
|
641
|
+
},
|
|
567
642
|
{
|
|
568
643
|
name: "vitest",
|
|
569
644
|
package: "vitest",
|
|
@@ -585,5 +660,6 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
|
|
|
585
660
|
{ property: "test.coverage.include", files: [VITEST_CONFIG] },
|
|
586
661
|
{ property: "test.coverage.exclude", files: [VITEST_CONFIG] },
|
|
587
662
|
],
|
|
663
|
+
testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
|
|
588
664
|
},
|
|
589
665
|
];
|
package/src/conventions.ts
CHANGED
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
type HelperReading,
|
|
54
54
|
type MoveReading,
|
|
55
55
|
type OptionsCall,
|
|
56
|
+
type PackageKey,
|
|
56
57
|
type ShortNameReading,
|
|
58
|
+
type TestFileReading,
|
|
57
59
|
} from "./convention-rows.ts";
|
|
58
60
|
import { documentStrings } from "./configuration-files.ts";
|
|
59
61
|
import { globExpression } from "./glob.ts";
|
|
@@ -118,6 +120,8 @@ export interface Conventions {
|
|
|
118
120
|
readonly uses: readonly string[];
|
|
119
121
|
/** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
|
|
120
122
|
readonly selected: ReadonlyMap<string, Selected>;
|
|
123
|
+
/** The files, absolute, an applied row's test-file properties name. */
|
|
124
|
+
readonly testFiles: ReadonlySet<string>;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
/** The strings one configuration file holds at selection keys, which root no file. */
|
|
@@ -607,6 +611,43 @@ function holdsValue(held: Node, value: true | string): boolean {
|
|
|
607
611
|
);
|
|
608
612
|
}
|
|
609
613
|
|
|
614
|
+
/** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
|
|
615
|
+
function testFileRoot(file: SourceFile, dir: string, reading: TestFileReading): string | undefined {
|
|
616
|
+
for (const property of reading.roots) {
|
|
617
|
+
const values = valuesAt(file, property, reading.call);
|
|
618
|
+
if (values.length > 0) {
|
|
619
|
+
const texts = new Set(values.map(literalText));
|
|
620
|
+
const [text] = texts;
|
|
621
|
+
return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return dir;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The files an applied row's test-file properties name. */
|
|
628
|
+
function testFilesNamed(
|
|
629
|
+
host: Host,
|
|
630
|
+
parse: ParseSource,
|
|
631
|
+
dir: string,
|
|
632
|
+
readings: readonly TestFileReading[],
|
|
633
|
+
into: Set<string>,
|
|
634
|
+
): void {
|
|
635
|
+
for (const reading of readings) {
|
|
636
|
+
for (const { file } of readingFiles(host, parse, dir, reading.files)) {
|
|
637
|
+
const at = testFileRoot(file, dir, reading);
|
|
638
|
+
if (at === undefined) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
for (const node of stringsAt(file, reading.property, reading.call)) {
|
|
642
|
+
const text = literalText(node);
|
|
643
|
+
if (text !== undefined && text !== "") {
|
|
644
|
+
into.add(resolvePath(at, text));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
610
651
|
/** The strings an applied row's selection keys hold, added per configuration file to `into`. */
|
|
611
652
|
function selectedStrings(
|
|
612
653
|
host: Host,
|
|
@@ -873,6 +914,60 @@ function helperUses(
|
|
|
873
914
|
.map((reading) => reading.package);
|
|
874
915
|
}
|
|
875
916
|
|
|
917
|
+
/**
|
|
918
|
+
* The dependencies one manifest declares that an applied row's package keys use: each
|
|
919
|
+
* whose name a string under a member of a key's name equals, or begins followed by `/` or
|
|
920
|
+
* by `:` and a name. A member's own name is no such string.
|
|
921
|
+
*/
|
|
922
|
+
function packageKeyUses(
|
|
923
|
+
host: Host,
|
|
924
|
+
parse: ParseSource,
|
|
925
|
+
dir: string,
|
|
926
|
+
manifest: unknown,
|
|
927
|
+
keys: readonly PackageKey[],
|
|
928
|
+
): readonly string[] {
|
|
929
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
930
|
+
const held = isRecord(manifest) ? manifest[section] : undefined;
|
|
931
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
932
|
+
});
|
|
933
|
+
const strings: string[] = [];
|
|
934
|
+
const collect = (node: Node): void => {
|
|
935
|
+
const text = literalText(node);
|
|
936
|
+
if (text !== undefined) {
|
|
937
|
+
strings.push(text);
|
|
938
|
+
} else if (isPropertyAssignment(node)) {
|
|
939
|
+
collect(node.initializer);
|
|
940
|
+
} else {
|
|
941
|
+
node.forEachChild(collect);
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
for (const { key, files } of keys) {
|
|
945
|
+
const visit = (node: Node): void => {
|
|
946
|
+
if (isPropertyAssignment(node) && keyOf(node) === key) {
|
|
947
|
+
collect(node.initializer);
|
|
948
|
+
}
|
|
949
|
+
node.forEachChild(visit);
|
|
950
|
+
};
|
|
951
|
+
for (const { file } of readingFiles(host, parse, dir, files)) {
|
|
952
|
+
const values = declaredValues(file);
|
|
953
|
+
for (const one of defaultExports(file)) {
|
|
954
|
+
const object = objectOf(one, values, true);
|
|
955
|
+
if (object !== undefined) {
|
|
956
|
+
visit(object);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return declared.filter((name) =>
|
|
962
|
+
strings.some(
|
|
963
|
+
(text) =>
|
|
964
|
+
text === name ||
|
|
965
|
+
text.startsWith(`${name}/`) ||
|
|
966
|
+
(text.startsWith(`${name}:`) && text.length > name.length + 1),
|
|
967
|
+
),
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
876
971
|
/** Two strings ordered bytewise. */
|
|
877
972
|
function compare(a: string, b: string): number {
|
|
878
973
|
if (a === b) {
|
|
@@ -898,6 +993,7 @@ export function readConventions(
|
|
|
898
993
|
const failures: ConventionFailure[] = [];
|
|
899
994
|
const uses = new Set<string>();
|
|
900
995
|
const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
|
|
996
|
+
const testFiles = new Set<string>();
|
|
901
997
|
for (const manifest of manifests) {
|
|
902
998
|
const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
|
|
903
999
|
const versions = new Map<string, string | undefined>();
|
|
@@ -959,6 +1055,10 @@ export function readConventions(
|
|
|
959
1055
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
960
1056
|
uses.add(name);
|
|
961
1057
|
}
|
|
1058
|
+
const keys = row.packageKeys ?? [];
|
|
1059
|
+
for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
|
|
1060
|
+
uses.add(name);
|
|
1061
|
+
}
|
|
962
1062
|
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
963
1063
|
uses.add(name);
|
|
964
1064
|
}
|
|
@@ -966,6 +1066,7 @@ export function readConventions(
|
|
|
966
1066
|
uses.add(name);
|
|
967
1067
|
}
|
|
968
1068
|
selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
|
|
1069
|
+
testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
|
|
969
1070
|
const excludes = [
|
|
970
1071
|
...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
|
|
971
1072
|
].map(globExpression);
|
|
@@ -996,6 +1097,7 @@ export function readConventions(
|
|
|
996
1097
|
failures,
|
|
997
1098
|
uses: [...uses].sort(compare),
|
|
998
1099
|
selected,
|
|
1100
|
+
testFiles,
|
|
999
1101
|
};
|
|
1000
1102
|
}
|
|
1001
1103
|
|
package/src/dependencies.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
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
9
|
|
|
10
10
|
import {
|
|
11
|
+
isCallExpression,
|
|
11
12
|
isExportDeclaration,
|
|
12
13
|
isExternalModuleReference,
|
|
13
14
|
isIdentifier,
|
|
14
15
|
isImportDeclaration,
|
|
15
16
|
isImportEqualsDeclaration,
|
|
17
|
+
isMetaProperty,
|
|
16
18
|
isNamespaceExport,
|
|
19
|
+
isPropertyAccessExpression,
|
|
17
20
|
isShorthandPropertyAssignment,
|
|
18
21
|
isStringLiteral,
|
|
19
22
|
isNoSubstitutionTemplateLiteral,
|
|
@@ -32,11 +35,13 @@ import {
|
|
|
32
35
|
import type { FindingPosition } from "./finding.ts";
|
|
33
36
|
import type { Host } from "./host.ts";
|
|
34
37
|
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
38
|
+
import { scriptTokens } from "./manifest.ts";
|
|
35
39
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
36
40
|
import type { DependencySection, Module } from "./ref.ts";
|
|
37
41
|
import { UNANSWERED } from "./query.ts";
|
|
38
42
|
import type { Handle, ProjectView } from "./session.ts";
|
|
39
43
|
import { sourceFilesOf } from "./source-files.ts";
|
|
44
|
+
import { workflowTokens } from "./workflow-steps.ts";
|
|
40
45
|
|
|
41
46
|
const MANIFEST = "package.json";
|
|
42
47
|
const MODULES_DIR = "node_modules";
|
|
@@ -54,7 +59,7 @@ const SECTIONS: readonly (readonly [string, DependencySection])[] = [
|
|
|
54
59
|
const PROJECT_OWNER = "";
|
|
55
60
|
|
|
56
61
|
/** One dependency a manifest the run reads declares. */
|
|
57
|
-
|
|
62
|
+
interface DeclaredDependency {
|
|
58
63
|
readonly name: string;
|
|
59
64
|
readonly section: DependencySection;
|
|
60
65
|
/** Where the manifest writes the dependency's key, below the target root. */
|
|
@@ -68,16 +73,14 @@ export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
|
|
|
68
73
|
return {
|
|
69
74
|
packages: new Set(packages),
|
|
70
75
|
uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
|
|
71
|
-
unanswered:
|
|
76
|
+
unanswered: [],
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/** What the installed copy of one declared dependency states about itself. */
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
readonly
|
|
79
|
-
/** The peer dependencies its manifest requires: every peer not marked optional. */
|
|
80
|
-
readonly peers: readonly string[];
|
|
81
|
+
interface InstalledDependency {
|
|
82
|
+
/** The commands its manifest's `bin` declares, by name. */
|
|
83
|
+
readonly commands: readonly string[];
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** What one project's own files and configuration need from outside the target. */
|
|
@@ -91,20 +94,29 @@ export interface ProjectNeeds {
|
|
|
91
94
|
*/
|
|
92
95
|
readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
|
|
93
96
|
/**
|
|
94
|
-
*
|
|
95
|
-
* dependency
|
|
97
|
+
* The files writing a module specifier the checker left unanswered, each of which may
|
|
98
|
+
* use any dependency a manifest at or above it declares.
|
|
96
99
|
*/
|
|
97
|
-
readonly unanswered:
|
|
100
|
+
readonly unanswered: readonly string[];
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/** The run's dependency evidence. */
|
|
101
104
|
export interface Dependencies {
|
|
102
105
|
/** Every declared dependency, manifest by manifest, in the order each writes them. */
|
|
103
106
|
readonly declared: readonly DeclaredDependency[];
|
|
104
|
-
/** The installed copy of each declared dependency the run found, by name. */
|
|
105
|
-
readonly installed: ReadonlyMap<string, InstalledDependency>;
|
|
106
107
|
/** Every package some project needs. */
|
|
107
108
|
readonly needed: ReadonlySet<string>;
|
|
109
|
+
/**
|
|
110
|
+
* The declared dependencies whose command a script entry of the manifest declaring them,
|
|
111
|
+
* or a workflow step for the target's manifest, runs, each by {@link declarationKey}.
|
|
112
|
+
*/
|
|
113
|
+
readonly ran: ReadonlySet<string>;
|
|
114
|
+
/**
|
|
115
|
+
* The manifests, by their path below the target root, whose directory holds source only
|
|
116
|
+
* in configurations the run could not build, so a dependency they declare that nothing
|
|
117
|
+
* needs is reported at `possible`.
|
|
118
|
+
*/
|
|
119
|
+
readonly unbuilt: ReadonlySet<string>;
|
|
108
120
|
/**
|
|
109
121
|
* Per deletion candidate, the declared dependencies whose last use in the target is
|
|
110
122
|
* written inside it, sorted bytewise; a candidate holding no last use has no entry.
|
|
@@ -325,9 +337,11 @@ export function projectNeeds<Brand>(
|
|
|
325
337
|
};
|
|
326
338
|
use([PROJECT_OWNER], packages);
|
|
327
339
|
|
|
328
|
-
|
|
340
|
+
const unanswered: string[] = [];
|
|
329
341
|
for (const file of project.ownSourceFiles()) {
|
|
330
|
-
|
|
342
|
+
if (fileNeeds(project, held, file, programPackages, use)) {
|
|
343
|
+
unanswered.push(file.fileName);
|
|
344
|
+
}
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
const beside = new Map<string, readonly string[]>();
|
|
@@ -352,16 +366,20 @@ export function projectNeeds<Brand>(
|
|
|
352
366
|
use(ownersOf(file, held, node), dependenciesNamed(text, names));
|
|
353
367
|
}
|
|
354
368
|
}
|
|
355
|
-
for (const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
369
|
+
for (const { strings, manifestDirs } of configuration.documents) {
|
|
370
|
+
for (const manifestDir of manifestDirs) {
|
|
371
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
372
|
+
for (const text of strings) {
|
|
373
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
374
|
+
}
|
|
359
375
|
}
|
|
360
376
|
}
|
|
361
|
-
for (const { file,
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
for (const { file, manifestDirs } of configuration.outside) {
|
|
378
|
+
for (const manifestDir of manifestDirs) {
|
|
379
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
380
|
+
for (const { text } of moduleStrings(file)) {
|
|
381
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
382
|
+
}
|
|
365
383
|
}
|
|
366
384
|
}
|
|
367
385
|
return { packages, uses, unanswered };
|
|
@@ -491,9 +509,43 @@ function fileNeeds<Brand>(
|
|
|
491
509
|
for (const reference of file.typeReferenceDirectives) {
|
|
492
510
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
493
511
|
}
|
|
512
|
+
for (const specifier of resolvedSpecifiers(file)) {
|
|
513
|
+
const named = packageOfSpecifier(specifier.text);
|
|
514
|
+
if (named !== undefined) {
|
|
515
|
+
use(ownersOf(file, held, specifier), [named]);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
494
518
|
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
495
519
|
}
|
|
496
520
|
|
|
521
|
+
/**
|
|
522
|
+
* The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
|
|
523
|
+
* or `require.resolve`, each of which needs the package it names installed.
|
|
524
|
+
*/
|
|
525
|
+
function resolvedSpecifiers(file: SourceFile): readonly Texted[] {
|
|
526
|
+
const found: Texted[] = [];
|
|
527
|
+
if (!file.text.includes(".resolve(")) {
|
|
528
|
+
return found;
|
|
529
|
+
}
|
|
530
|
+
const visit = (node: Node): void => {
|
|
531
|
+
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
532
|
+
const callee = node.expression;
|
|
533
|
+
const [argument] = node.arguments;
|
|
534
|
+
const receiver = callee.expression;
|
|
535
|
+
const resolving =
|
|
536
|
+
callee.name.text === "resolve" &&
|
|
537
|
+
((isMetaProperty(receiver) && receiver.name.text === "meta") ||
|
|
538
|
+
(isIdentifier(receiver) && receiver.text === "require"));
|
|
539
|
+
if (resolving && argument !== undefined && isLiteralText(argument)) {
|
|
540
|
+
found.push(argument);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
node.forEachChild(visit);
|
|
544
|
+
};
|
|
545
|
+
visit(file);
|
|
546
|
+
return found;
|
|
547
|
+
}
|
|
548
|
+
|
|
497
549
|
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
498
550
|
function valueEnd(text: string, at: number): number {
|
|
499
551
|
const first = text.charAt(at);
|
|
@@ -585,21 +637,17 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
|
|
|
585
637
|
for (let dir = root; ; dir = dirnamePath(dir)) {
|
|
586
638
|
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
587
639
|
if (read !== undefined && isRecord(read.value)) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const peers = manifest["peerDependencies"];
|
|
591
|
-
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
592
|
-
? manifest["peerDependenciesMeta"]
|
|
593
|
-
: {};
|
|
640
|
+
const bin = read.value["bin"];
|
|
641
|
+
// A string `bin` names one command, the package's name without its scope.
|
|
594
642
|
return {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
643
|
+
commands:
|
|
644
|
+
typeof bin === "string"
|
|
645
|
+
? bin === ""
|
|
646
|
+
? []
|
|
647
|
+
: [name.slice(name.indexOf("/") + 1)]
|
|
648
|
+
: isRecord(bin)
|
|
649
|
+
? Object.keys(bin)
|
|
650
|
+
: [],
|
|
603
651
|
};
|
|
604
652
|
}
|
|
605
653
|
if (dirnamePath(dir) === dir) {
|
|
@@ -639,12 +687,37 @@ function declaredIn(
|
|
|
639
687
|
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
640
688
|
}
|
|
641
689
|
|
|
690
|
+
/** The key one declared dependency is known by: its manifest's path and its name. */
|
|
691
|
+
export function declarationKey(dependency: DeclaredDependency): string {
|
|
692
|
+
return `${dependency.position.path}\u0000${dependency.name}`;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Every token of every value of one manifest's `scripts` member. */
|
|
696
|
+
function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string> {
|
|
697
|
+
const scripts = manifest["scripts"];
|
|
698
|
+
const found = new Set<string>();
|
|
699
|
+
if (isRecord(scripts)) {
|
|
700
|
+
for (const command of Object.values(scripts)) {
|
|
701
|
+
if (typeof command === "string") {
|
|
702
|
+
scriptTokens(command).forEach((token) => found.add(token));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return found;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
710
|
+
function isInside(path: string, dir: string): boolean {
|
|
711
|
+
return dir === "" || path.startsWith(`${dir}/`);
|
|
712
|
+
}
|
|
713
|
+
|
|
642
714
|
/**
|
|
643
715
|
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
644
716
|
* `members` below the target root, read with the position of every dependency each
|
|
645
|
-
* declares, the
|
|
646
|
-
*
|
|
647
|
-
* program cannot read declares
|
|
717
|
+
* declares, what the projects and the commands its scripts and workflow steps run need,
|
|
718
|
+
* and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
|
|
719
|
+
* in configurations the run could not build. A manifest the program cannot read declares
|
|
720
|
+
* nothing.
|
|
648
721
|
*/
|
|
649
722
|
export function dependenciesOf(
|
|
650
723
|
host: Host,
|
|
@@ -652,30 +725,43 @@ export function dependenciesOf(
|
|
|
652
725
|
perProject: readonly ProjectNeeds[],
|
|
653
726
|
candidates: readonly string[],
|
|
654
727
|
members: readonly string[] = [],
|
|
728
|
+
unbuiltMembers: readonly string[] = [],
|
|
655
729
|
): Dependencies {
|
|
656
730
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
657
731
|
const scopeOf = packageScope(host, targetRoot);
|
|
658
732
|
const manifests = new Map<string, string>([[root, MANIFEST]]);
|
|
659
|
-
|
|
733
|
+
const unbuilt = new Set<string>();
|
|
734
|
+
for (const dir of [...members, ...unbuiltMembers]) {
|
|
660
735
|
const below = relativePath(root, dir);
|
|
661
736
|
if (below !== undefined && !manifests.has(dir)) {
|
|
662
737
|
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
663
738
|
}
|
|
664
739
|
}
|
|
740
|
+
for (const dir of unbuiltMembers) {
|
|
741
|
+
const path = manifests.get(dir);
|
|
742
|
+
if (path !== undefined && dir !== root) {
|
|
743
|
+
unbuilt.add(path);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
665
747
|
const declared: DeclaredDependency[] = [];
|
|
666
|
-
const
|
|
748
|
+
const ran = new Set<string>();
|
|
667
749
|
for (const [dir, path] of manifests) {
|
|
668
750
|
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
669
751
|
if (read === undefined || !isRecord(read.value)) {
|
|
670
752
|
continue;
|
|
671
753
|
}
|
|
754
|
+
const tokens = scriptTokensOf(read.value);
|
|
672
755
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
673
756
|
declared.push(dependency);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
757
|
+
const found = installedOf(host, dir, dependency.name);
|
|
758
|
+
if (found === undefined) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
const runs = (command: string): boolean =>
|
|
762
|
+
tokens.has(command) || (dir === root && workflow.has(command));
|
|
763
|
+
if (found.commands.some(runs)) {
|
|
764
|
+
ran.add(declarationKey(dependency));
|
|
679
765
|
}
|
|
680
766
|
}
|
|
681
767
|
}
|
|
@@ -685,9 +771,15 @@ export function dependenciesOf(
|
|
|
685
771
|
const needed = new Set<string>();
|
|
686
772
|
const uses = new Map<string, Set<string>>();
|
|
687
773
|
for (const project of perProject) {
|
|
688
|
-
// A specifier the checker left unanswered may resolve to any
|
|
689
|
-
// and what the configuration needs is never deleted
|
|
690
|
-
const
|
|
774
|
+
// A specifier the checker left unanswered may resolve to any dependency a manifest at
|
|
775
|
+
// or above its file declares, and what the configuration needs is never deleted.
|
|
776
|
+
const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
|
|
777
|
+
const unknown = declared
|
|
778
|
+
.filter((dependency) => {
|
|
779
|
+
const dir = dirnamePath(dependency.position.path);
|
|
780
|
+
return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
|
|
781
|
+
})
|
|
782
|
+
.map((dependency) => dependency.name);
|
|
691
783
|
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
|
|
692
784
|
for (const [id, used] of [...project.uses, ...held]) {
|
|
693
785
|
const merged = uses.get(id) ?? new Set<string>();
|
|
@@ -711,5 +803,5 @@ export function dependenciesOf(
|
|
|
711
803
|
);
|
|
712
804
|
}
|
|
713
805
|
}
|
|
714
|
-
return { declared,
|
|
806
|
+
return { declared, needed, ran, unbuilt, lastUses };
|
|
715
807
|
}
|
package/src/discover.ts
CHANGED
|
@@ -34,7 +34,7 @@ export class DiscoveryError extends Error {
|
|
|
34
34
|
const ERROR_CATEGORY: number = DiagnosticCategory.Error;
|
|
35
35
|
|
|
36
36
|
/** Whether one diagnostic is an error, which is what fails a run closed. */
|
|
37
|
-
|
|
37
|
+
function isError(diagnostic: Diagnostic): boolean {
|
|
38
38
|
return diagnostic.category === ERROR_CATEGORY;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -113,7 +113,7 @@ export function projectErrors(sets: ProjectDiagnostics): ProjectErrors {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/** One project a run analyzes. */
|
|
116
|
-
|
|
116
|
+
interface DiscoveredProject {
|
|
117
117
|
/**
|
|
118
118
|
* The name a run gives the project: the identifier the build matrix declares for
|
|
119
119
|
* it, or, for a project discovery derived, its configuration file's path below the
|
|
@@ -162,7 +162,7 @@ export function isGuess(discovery: Discovery, configFile: string): boolean {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/** Every project discovery found, and where each came from. */
|
|
165
|
-
|
|
165
|
+
interface Discovery {
|
|
166
166
|
/** The projects, in discovery order, each once. */
|
|
167
167
|
readonly projects: readonly DiscoveredProject[];
|
|
168
168
|
/**
|
package/src/edges.ts
CHANGED
|
@@ -39,7 +39,7 @@ const REFERENCE_WANT =
|
|
|
39
39
|
"a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
40
40
|
|
|
41
41
|
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
42
|
-
|
|
42
|
+
class EdgesError extends ConfigError {
|
|
43
43
|
constructor(at: string, text: string, want: string) {
|
|
44
44
|
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
45
45
|
this.name = "EdgesError";
|
|
@@ -174,7 +174,7 @@ export function readEdgeSides(host: Host, targetRoot: string): readonly EdgeSide
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/** One side's verdict, as an evaluation names it. */
|
|
177
|
-
|
|
177
|
+
type EdgeState = "live" | "dead" | "absent";
|
|
178
178
|
|
|
179
179
|
/** One edge evaluation: one side of this language, its state, and the finding it holds. */
|
|
180
180
|
export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
|
|
@@ -184,7 +184,7 @@ export interface EdgeEvaluation<F extends Finding = Finding> extends EdgeSide {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/** The findings a report publishes, and one evaluation per declared side of this language. */
|
|
187
|
-
|
|
187
|
+
interface Evaluated<F extends Finding> {
|
|
188
188
|
readonly findings: readonly F[];
|
|
189
189
|
/** Ordered by edge, then by side, each compared bytewise. */
|
|
190
190
|
readonly evaluations: readonly EdgeEvaluation<F>[];
|