@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.
Files changed (45) hide show
  1. package/README.md +9 -16
  2. package/dist/src/analysis.js +142 -0
  3. package/dist/src/components.js +289 -0
  4. package/dist/src/config.js +6 -0
  5. package/dist/src/discover.js +98 -24
  6. package/dist/src/entry-point-gaps.js +278 -0
  7. package/dist/src/entry-points.js +385 -0
  8. package/dist/src/finding.js +12 -0
  9. package/dist/src/glob.js +65 -0
  10. package/dist/src/graph.js +116 -0
  11. package/dist/src/index.js +1 -2
  12. package/dist/src/manifest.js +120 -0
  13. package/dist/src/print.js +3 -6
  14. package/dist/src/references.js +156 -106
  15. package/dist/src/resolve.js +82 -20
  16. package/dist/src/roots.js +345 -0
  17. package/dist/src/run.js +59 -10
  18. package/dist/src/schema.js +1 -1
  19. package/dist/src/session.js +6 -1
  20. package/dist/src/source-files.js +122 -0
  21. package/dist/src/sweep.js +156 -0
  22. package/dist/src/version.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +3 -3
  25. package/src/analysis.ts +236 -0
  26. package/src/components.ts +372 -0
  27. package/src/config.ts +30 -2
  28. package/src/discover.ts +144 -29
  29. package/src/entry-point-gaps.ts +304 -0
  30. package/src/entry-points.ts +496 -0
  31. package/src/finding.ts +43 -0
  32. package/src/glob.ts +67 -0
  33. package/src/graph.ts +178 -0
  34. package/src/index.ts +1 -20
  35. package/src/manifest.ts +174 -0
  36. package/src/print.ts +5 -6
  37. package/src/references.ts +201 -111
  38. package/src/resolve.ts +117 -24
  39. package/src/roots.ts +467 -0
  40. package/src/run.ts +66 -11
  41. package/src/schema.ts +5 -2
  42. package/src/session.ts +6 -1
  43. package/src/source-files.ts +157 -0
  44. package/src/sweep.ts +230 -0
  45. package/src/version.ts +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Glob patterns over the paths of the target's own files, as the configuration
3
+ * writes them. Both `ts.test_files` and `ts.entry_files` name files this way, and
4
+ * one grammar answers for both.
5
+ *
6
+ * A configured root is not written in this grammar. It names a symbol reference
7
+ * rather than a path, `*` there spans the solidus and a brace list means nothing,
8
+ * so that rule is written where it is read.
9
+ */
10
+ /**
11
+ * One literal run of a pattern, with every character the expression syntax claims
12
+ * escaped.
13
+ *
14
+ * The set is the syntax characters and the solidus, and it is closed: an escape of any
15
+ * other character is refused outright by the expression syntax under the unicode flag,
16
+ * so escaping a hyphen, which needs none outside a character class, would refuse every
17
+ * pattern that names a hyphenated path.
18
+ */
19
+ function quoted(text) {
20
+ return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
21
+ }
22
+ /**
23
+ * The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
24
+ * do not, a brace list is an alternation of its members, and every other character
25
+ * stands for itself.
26
+ */
27
+ export function globExpression(pattern) {
28
+ let source = "^";
29
+ let at = 0;
30
+ while (at < pattern.length) {
31
+ const rest = pattern.slice(at);
32
+ if (rest.startsWith("**/")) {
33
+ source += "(?:[^/]*/)*";
34
+ at += 3;
35
+ continue;
36
+ }
37
+ if (rest.startsWith("**")) {
38
+ source += ".*";
39
+ at += 2;
40
+ continue;
41
+ }
42
+ const character = pattern[at] ?? "";
43
+ at += 1;
44
+ if (character === "*") {
45
+ source += "[^/]*";
46
+ continue;
47
+ }
48
+ if (character === "?") {
49
+ source += "[^/]";
50
+ continue;
51
+ }
52
+ if (character === "{") {
53
+ const close = pattern.indexOf("}", at);
54
+ if (close < 0) {
55
+ source += "\\{";
56
+ continue;
57
+ }
58
+ source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
59
+ at = close + 1;
60
+ continue;
61
+ }
62
+ source += quoted(character);
63
+ }
64
+ return new RegExp(`${source}$`, "u");
65
+ }
@@ -0,0 +1,116 @@
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
+ /** The position of a declaration the inventory does not hold. */
8
+ export const OUTSIDE = -1;
9
+ /**
10
+ * Whether the sweep judges a declaration of each kind. A file is not judged by either
11
+ * relation: whether a file is built and whether anything imports it are the subjects of
12
+ * the file kinds, which read the program rather than ask whether anything references the
13
+ * file. The Contract's finding schema names the kinds that carry no liveness relation,
14
+ * and a test holds this table to that list.
15
+ */
16
+ const SWEPT = {
17
+ file: false,
18
+ function: true,
19
+ class: true,
20
+ interface: true,
21
+ type: true,
22
+ enum: true,
23
+ namespace: true,
24
+ variable: true,
25
+ "export-alias": true,
26
+ method: true,
27
+ "class-member": true,
28
+ "interface-method": true,
29
+ "type-member": true,
30
+ "enum-member": true,
31
+ "type-parameter": true,
32
+ };
33
+ /**
34
+ * Whether a root of each kind names a caller, which holds it live under both relations: a
35
+ * tool reads the configuration it loads, a runtime runs a test file, a setup file and a
36
+ * worker, and a configured root asserts a caller. The other kinds only suppose a consumer
37
+ * that imports what a manifest entry, a binary or an entry file exports, or names a
38
+ * library's published API, so such a root is live under reachability alone.
39
+ */
40
+ const NAMES_A_CALLER = {
41
+ "entry-file": false,
42
+ "manifest-entry": false,
43
+ "manifest-binary": false,
44
+ "published-api": false,
45
+ configured: true,
46
+ pattern: true,
47
+ "test-runner": true,
48
+ "lint-configuration": true,
49
+ "mutation-testing": true,
50
+ "browser-tests": true,
51
+ worker: true,
52
+ };
53
+ /** Whether the sweep judges a declaration of one kind. */
54
+ export function swept(kind) {
55
+ return SWEPT[kind];
56
+ }
57
+ /** Whether a root of one kind names a caller rather than supposing one. */
58
+ export function namesACaller(kind) {
59
+ return NAMES_A_CALLER[kind];
60
+ }
61
+ /**
62
+ * Indexes one project's inventory, references and roots. `testFiles` are the paths the
63
+ * reference pass classified as test files, so a declaration one holds is a test
64
+ * declaration. A reference to a declaration the inventory does not hold is dropped; one
65
+ * from such a declaration still counts toward its target, because reference counting
66
+ * asks whether any declaration names the target. A root naming nothing is dropped.
67
+ */
68
+ export function graphOf(symbols, references, roots, testFiles) {
69
+ const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
70
+ const at = (id) => index.get(id) ?? OUTSIDE;
71
+ const tests = new Set(testFiles);
72
+ const out = symbols.map(() => []);
73
+ const made = symbols.map(() => ({ production: 0, test: 0 }));
74
+ for (const reference of references) {
75
+ const to = at(reference.to);
76
+ if (to === OUTSIDE) {
77
+ continue;
78
+ }
79
+ const counts = made[to];
80
+ if (counts !== undefined) {
81
+ if (reference.test) {
82
+ counts.test += 1;
83
+ }
84
+ else {
85
+ counts.production += 1;
86
+ }
87
+ }
88
+ out[at(reference.from)]?.push({ to, test: reference.test });
89
+ }
90
+ const parent = symbols.map((symbol) => at(symbol.parent));
91
+ const exportsOf = symbols.map(() => []);
92
+ symbols.forEach((symbol, position) => {
93
+ const container = parent[position] ?? OUTSIDE;
94
+ if (symbol.exported && symbols[container]?.kind === "file") {
95
+ exportsOf[container]?.push(position);
96
+ }
97
+ });
98
+ const rooted = [];
99
+ for (const root of roots) {
100
+ const position = at(root.id);
101
+ if (position !== OUTSIDE) {
102
+ rooted.push({ at: position, kind: root.kind });
103
+ }
104
+ }
105
+ return {
106
+ symbols,
107
+ out,
108
+ made,
109
+ parent,
110
+ test: symbols.map((symbol) => tests.has(symbol.position.path)),
111
+ subject: symbols.map((symbol) => swept(symbol.kind)),
112
+ exportsOf,
113
+ rooted,
114
+ at,
115
+ };
116
+ }
package/dist/src/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export { run, SETTING_OPTIONS } from "./run.js";
2
- export { inventory, nodeKey, } from "./inventory.js";
3
- export { DEFAULT_BATCH_CAP, references, } from "./references.js";
2
+ export { DECLINED_CONVENTIONS } from "./entry-point-gaps.js";
4
3
  export { byPosition, positionKey, PositionError, renderPosition, } from "./position.js";
5
4
  export { computedComponent, isRef, nameComponent, REF_EXPRESSIONS, renderRef, } from "./ref.js";
6
5
  export { CONTRACT_VERSION } from "./version.js";
@@ -0,0 +1,120 @@
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
+ import { isAbsolutePath, joinPath, resolvePath } from "./paths.js";
11
+ /** The manifest's own name. */
12
+ const MANIFEST = "package.json";
13
+ /** The three members that name an entry point beside `exports` and `bin`. */
14
+ const LEGACY_MEMBERS = ["main", "module", "types"];
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ /**
19
+ * Whether one member's value names a file of this package rather than another
20
+ * package or a URL. A target written with no leading dot is a path too, which is
21
+ * how the members that preceded `exports` are written.
22
+ */
23
+ function isTarget(value, requireDot) {
24
+ if (value === "" || isAbsolutePath(value) || value.includes("://")) {
25
+ return false;
26
+ }
27
+ return !requireDot || value.startsWith("./");
28
+ }
29
+ /** One member's spelling below another: `exports` then `exports["."]`. */
30
+ function below(member, key) {
31
+ return `${member}[${JSON.stringify(key)}]`;
32
+ }
33
+ /**
34
+ * Every target one `exports` value carries, at the member each is written under.
35
+ *
36
+ * The value is a string, an object whose keys are subpaths or conditions, or an
37
+ * array of alternatives, nested to any depth, and a `null` blocks a subpath rather
38
+ * than naming a target. Every level is walked because a condition names a different
39
+ * file of the same package and each is an entry point of its own; a key's meaning
40
+ * is not read, because a subpath and a condition both lead to a target and the
41
+ * walk needs no more than that.
42
+ */
43
+ function exportTargets(value, member, found) {
44
+ if (typeof value === "string") {
45
+ if (isTarget(value, true)) {
46
+ found.push({ member, path: value, role: "import", published: true });
47
+ }
48
+ return;
49
+ }
50
+ if (Array.isArray(value)) {
51
+ value.forEach((alternative, index) => {
52
+ exportTargets(alternative, below(member, String(index)), found);
53
+ });
54
+ return;
55
+ }
56
+ if (!isRecord(value)) {
57
+ return;
58
+ }
59
+ for (const [key, held] of Object.entries(value)) {
60
+ exportTargets(held, below(member, key), found);
61
+ }
62
+ }
63
+ /** The manifest at the target root, decoded, or undefined where there is none to read. */
64
+ function manifestOf(host, targetRoot) {
65
+ let text;
66
+ try {
67
+ text = host.readFile(joinPath(targetRoot, MANIFEST));
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ let value;
73
+ try {
74
+ value = JSON.parse(text);
75
+ }
76
+ catch {
77
+ // A manifest this program cannot read names no entry point, which is the same
78
+ // answer as an absent one. What a manifest's contents say is not refused here:
79
+ // the package manager owns that refusal, and an analysis of a tree it rejects
80
+ // is not the run's subject.
81
+ return undefined;
82
+ }
83
+ return isRecord(value) ? value : undefined;
84
+ }
85
+ /**
86
+ * What the target's own manifest declares about where the package is entered.
87
+ *
88
+ * A target is taken as written. It is not resolved to a source file here, because
89
+ * which file a target names is a fact about the compiler configuration that emits
90
+ * it and there is one such configuration per project, while there is one manifest.
91
+ */
92
+ export function readManifest(host, targetRoot) {
93
+ const manifest = manifestOf(host, targetRoot);
94
+ if (manifest === undefined) {
95
+ return { entries: [], declaresExports: false };
96
+ }
97
+ const found = [];
98
+ for (const member of LEGACY_MEMBERS) {
99
+ const value = manifest[member];
100
+ if (typeof value === "string" && isTarget(value, false)) {
101
+ found.push({ member, path: value, role: "import", published: true });
102
+ }
103
+ }
104
+ const bin = manifest["bin"];
105
+ if (typeof bin === "string" && isTarget(bin, false)) {
106
+ found.push({ member: "bin", path: bin, role: "run", published: false });
107
+ }
108
+ else if (isRecord(bin)) {
109
+ for (const [command, value] of Object.entries(bin)) {
110
+ if (typeof value === "string" && isTarget(value, false)) {
111
+ found.push({ member: below("bin", command), path: value, role: "run", published: false });
112
+ }
113
+ }
114
+ }
115
+ exportTargets(manifest["exports"], "exports", found);
116
+ return {
117
+ entries: found.map((entry) => ({ ...entry, path: resolvePath(targetRoot, entry.path) })),
118
+ declaresExports: manifest["exports"] !== undefined,
119
+ };
120
+ }
package/dist/src/print.js CHANGED
@@ -19,12 +19,9 @@ export function printConfig(config, provenance) {
19
19
  min_confidence: config.analysis.minConfidence,
20
20
  generated_files: config.analysis.generatedFiles,
21
21
  consumer_tests: config.analysis.consumerTests,
22
- configurations: config.analysis.configurations.map((entry) => ({
23
- id: entry.id,
24
- os: entry.os,
25
- arch: entry.arch,
26
- tags: entry.tags,
27
- })),
22
+ configurations: config.analysis.configurations.map((entry) => entry.shape === "project"
23
+ ? { id: entry.id, project: entry.project }
24
+ : { id: entry.id, os: entry.os, arch: entry.arch, tags: entry.tags }),
28
25
  matrix: { complete: config.analysis.matrixComplete },
29
26
  template_dirs: config.analysis.templateDirs,
30
27
  template_delimiters: {