@cplieger/deadset-ts 2.2.1 → 3.0.0
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/README.md +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
package/src/graph.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One project's declarations, the references between them and the roots that seed
|
|
3
|
+
* reachability, indexed for the sweep. The graph reads no file and asks the compiler
|
|
4
|
+
* nothing, so a sweep over it is a function of the values it was built from, and the
|
|
5
|
+
* inventory's order is the order every set a sweep answers reads in.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { InventorySymbol, SymbolKind } from "./inventory.ts";
|
|
9
|
+
import type { Reference } from "./references.ts";
|
|
10
|
+
import type { Root, RootKind } from "./roots.ts";
|
|
11
|
+
|
|
12
|
+
/** The position of a declaration the inventory does not hold. */
|
|
13
|
+
export const OUTSIDE = -1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether the sweep judges a declaration of each kind. A file is not judged by either
|
|
17
|
+
* relation: whether a file is built and whether anything imports it are the subjects of
|
|
18
|
+
* the file kinds, which read the program rather than ask whether anything references the
|
|
19
|
+
* file. The Contract's finding schema names the kinds that carry no liveness relation,
|
|
20
|
+
* and a test holds this table to that list.
|
|
21
|
+
*/
|
|
22
|
+
const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
23
|
+
file: false,
|
|
24
|
+
function: true,
|
|
25
|
+
class: true,
|
|
26
|
+
interface: true,
|
|
27
|
+
type: true,
|
|
28
|
+
enum: true,
|
|
29
|
+
namespace: true,
|
|
30
|
+
variable: true,
|
|
31
|
+
"export-alias": true,
|
|
32
|
+
method: true,
|
|
33
|
+
"class-member": true,
|
|
34
|
+
"interface-method": true,
|
|
35
|
+
"type-member": true,
|
|
36
|
+
"enum-member": true,
|
|
37
|
+
"type-parameter": true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
|
+
* tool reads the configuration it loads, a runtime runs a test file, a setup file and a
|
|
43
|
+
* worker, and a configured root asserts a caller. The other kinds only suppose a consumer
|
|
44
|
+
* that imports what a manifest entry, a binary or an entry file exports, or names a
|
|
45
|
+
* library's published API, so such a root is live under reachability alone.
|
|
46
|
+
*/
|
|
47
|
+
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
48
|
+
"entry-file": false,
|
|
49
|
+
"manifest-entry": false,
|
|
50
|
+
"manifest-binary": false,
|
|
51
|
+
"published-api": false,
|
|
52
|
+
configured: true,
|
|
53
|
+
pattern: true,
|
|
54
|
+
"test-runner": true,
|
|
55
|
+
"lint-configuration": true,
|
|
56
|
+
"mutation-testing": true,
|
|
57
|
+
"browser-tests": true,
|
|
58
|
+
worker: true,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Whether the sweep judges a declaration of one kind. */
|
|
62
|
+
export function swept(kind: SymbolKind): boolean {
|
|
63
|
+
return SWEPT[kind];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Whether a root of one kind names a caller rather than supposing one. */
|
|
67
|
+
export function namesACaller(kind: RootKind): boolean {
|
|
68
|
+
return NAMES_A_CALLER[kind];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One reference as the sweep reads it: the declaration named, and whether a test file made it. */
|
|
72
|
+
export interface Edge {
|
|
73
|
+
readonly to: number;
|
|
74
|
+
readonly test: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How many references one declaration carries, split by the classification of the file that made each. */
|
|
78
|
+
export interface Counts {
|
|
79
|
+
readonly production: number;
|
|
80
|
+
readonly test: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One root the inventory holds, at the position of the declaration it names. */
|
|
84
|
+
export interface Rooted {
|
|
85
|
+
readonly at: number;
|
|
86
|
+
readonly kind: RootKind;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The index the sweep and the component pass read. */
|
|
90
|
+
export interface Graph {
|
|
91
|
+
/** The declarations, in the order the inventory returns them. */
|
|
92
|
+
readonly symbols: readonly InventorySymbol[];
|
|
93
|
+
/** Per declaration, the references it makes to declarations the inventory holds. */
|
|
94
|
+
readonly out: readonly (readonly Edge[])[];
|
|
95
|
+
/** Per declaration, the references made to it, whoever made them. */
|
|
96
|
+
readonly made: readonly Counts[];
|
|
97
|
+
/** Per declaration, the position of its container, or {@link OUTSIDE} for a file. */
|
|
98
|
+
readonly parent: readonly number[];
|
|
99
|
+
/** Per declaration, whether a test file holds it. */
|
|
100
|
+
readonly test: readonly boolean[];
|
|
101
|
+
/** Per declaration, whether the sweep judges its liveness. */
|
|
102
|
+
readonly subject: readonly boolean[];
|
|
103
|
+
/**
|
|
104
|
+
* Per declaration, the declarations its export table names where it is a file, and
|
|
105
|
+
* nothing for any other declaration. A reference to a file is a reference to the
|
|
106
|
+
* module's namespace, which reaches everything the module exports.
|
|
107
|
+
*/
|
|
108
|
+
readonly exportsOf: readonly (readonly number[])[];
|
|
109
|
+
/** Every root naming a declaration the inventory holds, in the order the root set gives them. */
|
|
110
|
+
readonly rooted: readonly Rooted[];
|
|
111
|
+
/** The position of the declaration one identifier names, or {@link OUTSIDE}. */
|
|
112
|
+
at(id: string): number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Indexes one project's inventory, references and roots. `testFiles` are the paths the
|
|
117
|
+
* reference pass classified as test files, so a declaration one holds is a test
|
|
118
|
+
* declaration. A reference to a declaration the inventory does not hold is dropped; one
|
|
119
|
+
* from such a declaration still counts toward its target, because reference counting
|
|
120
|
+
* asks whether any declaration names the target. A root naming nothing is dropped.
|
|
121
|
+
*/
|
|
122
|
+
export function graphOf(
|
|
123
|
+
symbols: readonly InventorySymbol[],
|
|
124
|
+
references: readonly Reference[],
|
|
125
|
+
roots: readonly Root[],
|
|
126
|
+
testFiles: readonly string[],
|
|
127
|
+
): Graph {
|
|
128
|
+
const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
|
|
129
|
+
const at = (id: string): number => index.get(id) ?? OUTSIDE;
|
|
130
|
+
const tests = new Set(testFiles);
|
|
131
|
+
|
|
132
|
+
const out: Edge[][] = symbols.map(() => []);
|
|
133
|
+
const made = symbols.map(() => ({ production: 0, test: 0 }));
|
|
134
|
+
for (const reference of references) {
|
|
135
|
+
const to = at(reference.to);
|
|
136
|
+
if (to === OUTSIDE) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const counts = made[to];
|
|
140
|
+
if (counts !== undefined) {
|
|
141
|
+
if (reference.test) {
|
|
142
|
+
counts.test += 1;
|
|
143
|
+
} else {
|
|
144
|
+
counts.production += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
out[at(reference.from)]?.push({ to, test: reference.test });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parent = symbols.map((symbol) => at(symbol.parent));
|
|
151
|
+
const exportsOf: number[][] = symbols.map(() => []);
|
|
152
|
+
symbols.forEach((symbol, position) => {
|
|
153
|
+
const container = parent[position] ?? OUTSIDE;
|
|
154
|
+
if (symbol.exported && symbols[container]?.kind === "file") {
|
|
155
|
+
exportsOf[container]?.push(position);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const rooted: Rooted[] = [];
|
|
160
|
+
for (const root of roots) {
|
|
161
|
+
const position = at(root.id);
|
|
162
|
+
if (position !== OUTSIDE) {
|
|
163
|
+
rooted.push({ at: position, kind: root.kind });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
symbols,
|
|
169
|
+
out,
|
|
170
|
+
made,
|
|
171
|
+
parent,
|
|
172
|
+
test: symbols.map((symbol) => tests.has(symbol.position.path)),
|
|
173
|
+
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
174
|
+
exportsOf,
|
|
175
|
+
rooted,
|
|
176
|
+
at,
|
|
177
|
+
};
|
|
178
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
|
|
2
2
|
export type { DirectoryEntry, Host, PathKind } from "./host.ts";
|
|
3
|
-
export {
|
|
4
|
-
inventory,
|
|
5
|
-
nodeKey,
|
|
6
|
-
type Inventory,
|
|
7
|
-
type InventoryCost,
|
|
8
|
-
type InventorySymbol,
|
|
9
|
-
type SymbolKind,
|
|
10
|
-
type Visibility,
|
|
11
|
-
} from "./inventory.ts";
|
|
12
|
-
export {
|
|
13
|
-
DEFAULT_BATCH_CAP,
|
|
14
|
-
references,
|
|
15
|
-
type Reference,
|
|
16
|
-
type ReferenceCost,
|
|
17
|
-
type ReferenceOptions,
|
|
18
|
-
type References,
|
|
19
|
-
type Resolution,
|
|
20
|
-
type TestFileRule,
|
|
21
|
-
type Use,
|
|
22
|
-
} from "./references.ts";
|
|
3
|
+
export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
|
|
23
4
|
export {
|
|
24
5
|
byPosition,
|
|
25
6
|
positionKey,
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry points a package manifest names, which are roots because something
|
|
3
|
+
* outside the analyzed program reaches them: a consumer importing the package, or a
|
|
4
|
+
* shell running its command.
|
|
5
|
+
*
|
|
6
|
+
* Only the target's own manifest is read. A manifest below the target root belongs
|
|
7
|
+
* to another package, whose entry points are that package's roots and not this
|
|
8
|
+
* target's.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Host } from "./host.ts";
|
|
12
|
+
import { isAbsolutePath, joinPath, resolvePath } from "./paths.ts";
|
|
13
|
+
|
|
14
|
+
/** The manifest's own name. */
|
|
15
|
+
const MANIFEST = "package.json";
|
|
16
|
+
|
|
17
|
+
/** The three members that name an entry point beside `exports` and `bin`. */
|
|
18
|
+
const LEGACY_MEMBERS = ["main", "module", "types"] as const;
|
|
19
|
+
|
|
20
|
+
/** What reaches one entry point the manifest names. */
|
|
21
|
+
export type EntryRole =
|
|
22
|
+
/** A consumer importing the package, which is a caller the analysis cannot see. */
|
|
23
|
+
| "import"
|
|
24
|
+
/** A shell running the package's command, which is a caller that exists. */
|
|
25
|
+
| "run";
|
|
26
|
+
|
|
27
|
+
/** One entry point the manifest names. */
|
|
28
|
+
export interface ManifestEntry {
|
|
29
|
+
/**
|
|
30
|
+
* The member that named it, spelled as a reader of the manifest reads one:
|
|
31
|
+
* `main`, `bin["deadset-ts"]`, `exports["."]["import"]`. It is what a maintainer
|
|
32
|
+
* edits, so it is what a root reports as the string that named it.
|
|
33
|
+
*/
|
|
34
|
+
readonly member: string;
|
|
35
|
+
/** The target the member carries, resolved against the manifest's directory. */
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly role: EntryRole;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the member declares what a consumer may import, which is `exports` and
|
|
40
|
+
* the three members that preceded it. A command is published too, and is not this:
|
|
41
|
+
* `exports` is the surface a library's published API is read from.
|
|
42
|
+
*/
|
|
43
|
+
readonly published: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether one member's value names a file of this package rather than another
|
|
52
|
+
* package or a URL. A target written with no leading dot is a path too, which is
|
|
53
|
+
* how the members that preceded `exports` are written.
|
|
54
|
+
*/
|
|
55
|
+
function isTarget(value: string, requireDot: boolean): boolean {
|
|
56
|
+
if (value === "" || isAbsolutePath(value) || value.includes("://")) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return !requireDot || value.startsWith("./");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One member's spelling below another: `exports` then `exports["."]`. */
|
|
63
|
+
function below(member: string, key: string): string {
|
|
64
|
+
return `${member}[${JSON.stringify(key)}]`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every target one `exports` value carries, at the member each is written under.
|
|
69
|
+
*
|
|
70
|
+
* The value is a string, an object whose keys are subpaths or conditions, or an
|
|
71
|
+
* array of alternatives, nested to any depth, and a `null` blocks a subpath rather
|
|
72
|
+
* than naming a target. Every level is walked because a condition names a different
|
|
73
|
+
* file of the same package and each is an entry point of its own; a key's meaning
|
|
74
|
+
* is not read, because a subpath and a condition both lead to a target and the
|
|
75
|
+
* walk needs no more than that.
|
|
76
|
+
*/
|
|
77
|
+
function exportTargets(value: unknown, member: string, found: ManifestEntry[]): void {
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
if (isTarget(value, true)) {
|
|
80
|
+
found.push({ member, path: value, role: "import", published: true });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
value.forEach((alternative, index) => {
|
|
86
|
+
exportTargets(alternative, below(member, String(index)), found);
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!isRecord(value)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const [key, held] of Object.entries(value)) {
|
|
94
|
+
exportTargets(held, below(member, key), found);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The manifest at the target root, decoded, or undefined where there is none to read. */
|
|
99
|
+
function manifestOf(host: Host, targetRoot: string): Record<string, unknown> | undefined {
|
|
100
|
+
let text: string;
|
|
101
|
+
try {
|
|
102
|
+
text = host.readFile(joinPath(targetRoot, MANIFEST));
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
let value: unknown;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
// A manifest this program cannot read names no entry point, which is the same
|
|
111
|
+
// answer as an absent one. What a manifest's contents say is not refused here:
|
|
112
|
+
// the package manager owns that refusal, and an analysis of a tree it rejects
|
|
113
|
+
// is not the run's subject.
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return isRecord(value) ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** What the target's own manifest declares about where the package is entered. */
|
|
120
|
+
export interface Manifest {
|
|
121
|
+
/**
|
|
122
|
+
* Every entry point the manifest names, in the order the members are read: `main`,
|
|
123
|
+
* `module`, `types`, then each `bin` command, then every target of `exports`. Each
|
|
124
|
+
* path is absolute.
|
|
125
|
+
*/
|
|
126
|
+
readonly entries: readonly ManifestEntry[];
|
|
127
|
+
/**
|
|
128
|
+
* Whether an `exports` member is written at all. A package that declares one has
|
|
129
|
+
* said exactly what a consumer may import, so its published API is what `exports`
|
|
130
|
+
* reaches; a package that declares none publishes whatever the members that
|
|
131
|
+
* preceded it name.
|
|
132
|
+
*/
|
|
133
|
+
readonly declaresExports: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What the target's own manifest declares about where the package is entered.
|
|
138
|
+
*
|
|
139
|
+
* A target is taken as written. It is not resolved to a source file here, because
|
|
140
|
+
* which file a target names is a fact about the compiler configuration that emits
|
|
141
|
+
* it and there is one such configuration per project, while there is one manifest.
|
|
142
|
+
*/
|
|
143
|
+
export function readManifest(host: Host, targetRoot: string): Manifest {
|
|
144
|
+
const manifest = manifestOf(host, targetRoot);
|
|
145
|
+
if (manifest === undefined) {
|
|
146
|
+
return { entries: [], declaresExports: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const found: ManifestEntry[] = [];
|
|
150
|
+
for (const member of LEGACY_MEMBERS) {
|
|
151
|
+
const value = manifest[member];
|
|
152
|
+
if (typeof value === "string" && isTarget(value, false)) {
|
|
153
|
+
found.push({ member, path: value, role: "import", published: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const bin = manifest["bin"];
|
|
158
|
+
if (typeof bin === "string" && isTarget(bin, false)) {
|
|
159
|
+
found.push({ member: "bin", path: bin, role: "run", published: false });
|
|
160
|
+
} else if (isRecord(bin)) {
|
|
161
|
+
for (const [command, value] of Object.entries(bin)) {
|
|
162
|
+
if (typeof value === "string" && isTarget(value, false)) {
|
|
163
|
+
found.push({ member: below("bin", command), path: value, role: "run", published: false });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
exportTargets(manifest["exports"], "exports", found);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
|
|
172
|
+
declaresExports: manifest["exports"] !== undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/print.ts
CHANGED
|
@@ -20,12 +20,11 @@ export function printConfig(config: Config, provenance: Provenance): string {
|
|
|
20
20
|
min_confidence: config.analysis.minConfidence,
|
|
21
21
|
generated_files: config.analysis.generatedFiles,
|
|
22
22
|
consumer_tests: config.analysis.consumerTests,
|
|
23
|
-
configurations: config.analysis.configurations.map((entry) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})),
|
|
23
|
+
configurations: config.analysis.configurations.map((entry) =>
|
|
24
|
+
entry.shape === "project"
|
|
25
|
+
? { id: entry.id, project: entry.project }
|
|
26
|
+
: { id: entry.id, os: entry.os, arch: entry.arch, tags: entry.tags },
|
|
27
|
+
),
|
|
29
28
|
matrix: { complete: config.analysis.matrixComplete },
|
|
30
29
|
template_dirs: config.analysis.templateDirs,
|
|
31
30
|
template_delimiters: {
|