@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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 (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -1,6 +1,8 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
+ import { WORDS } from "../inventory.js";
2
3
  import { positionKey } from "../position.js";
3
4
  import { declarationKey } from "../ref.js";
5
+ import { reachabilityClasses } from "./completion.js";
4
6
  import { bytewise } from "../text/compare.js";
5
7
  /** The containers whose type parameters `DS1006` reports: a function, a class method and a type alias. */
6
8
  const SIGNATURES = new Set(["function", "method", "type"]);
@@ -70,14 +72,16 @@ function parametersOnlyNamedBy(run, namedBy, member) {
70
72
  .map((symbol) => symbol.name);
71
73
  }
72
74
  /** What one write-only finding says, the declarations its deletion takes with it included. */
73
- function writeOnlyMessage(run, namedBy, symbol, held) {
75
+ function writeOnlyMessage(run, written, subject) {
76
+ const { symbol, held } = subject;
74
77
  const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
75
78
  const read = run.input.stores.readInTests.has(symbol.id)
76
79
  ? "read only from test files"
77
80
  : "never read";
78
81
  const cascade = [
79
- ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
82
+ ...parametersOnlyNamedBy(run, written.namedBy, symbol).map((name) => `type parameter ${name}`),
80
83
  ...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
84
+ ...(written.named.get(symbol.id) ?? []),
81
85
  ];
82
86
  const word = SUBJECT_WORDS[symbol.kind] ?? "member";
83
87
  const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
@@ -86,16 +90,34 @@ function writeOnlyMessage(run, namedBy, symbol, held) {
86
90
  : `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
87
91
  }
88
92
  /**
89
- * `DS1301`: a module-level variable, a type member or a class member the counted
90
- * references store into and never read, named with every position written.
93
+ * The kinds of declaration that fall with a write-only subject: a declaration of its own.
94
+ * A member falls with its container, and a type parameter is the type-parameter clause's.
95
+ */
96
+ const FALLING = new Set([
97
+ "function",
98
+ "class",
99
+ "interface",
100
+ "type",
101
+ "enum",
102
+ "namespace",
103
+ "variable",
104
+ ]);
105
+ const WRITTEN = new WeakMap();
106
+ /**
107
+ * `DS1301`'s subjects: a module-level variable, a type member or a class member the
108
+ * counted references store into and never read.
91
109
  *
92
110
  * The subject is a live declaration: a store is a reference, so the sweep holds a
93
111
  * written one live and its candidates are never this kind's. Every reference that is
94
112
  * not a store reads, and so does an exemption record naming the declaration.
95
113
  */
96
- function writeOnlySymbols(run) {
114
+ function writtenOf(run) {
115
+ const known = WRITTEN.get(run.input);
116
+ if (known !== undefined) {
117
+ return known;
118
+ }
97
119
  const { stores } = run.input;
98
- const { matrix } = run.input.swept;
120
+ const { matrix, sweep } = run.input.swept;
99
121
  const uses = new Map();
100
122
  for (const reference of stores.references) {
101
123
  let held = uses.get(reference.to);
@@ -124,22 +146,49 @@ function writeOnlySymbols(run) {
124
146
  namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
125
147
  }
126
148
  }
127
- const found = [];
128
- for (const symbol of matrix.union.symbols) {
149
+ const subjects = matrix.union.symbols.flatMap((symbol) => {
129
150
  const held = uses.get(symbol.id);
130
- if (held === undefined ||
151
+ return held === undefined ||
131
152
  held.writes.length === 0 ||
132
153
  held.reads > 0 ||
133
154
  !writable(run, symbol) ||
134
155
  stores.exempt.has(symbol.id) ||
135
- run.candidates.has(symbol.id)) {
156
+ run.candidates.has(symbol.id)
157
+ ? []
158
+ : [{ symbol, held }];
159
+ });
160
+ const subjectIds = new Set(subjects.map((subject) => subject.symbol.id));
161
+ const classOf = reachabilityClasses(run.input);
162
+ const named = new Map();
163
+ const falls = new Set();
164
+ for (const symbol of matrix.union.symbols) {
165
+ const chains = sweep.carried.get(symbol.id);
166
+ if (chains === undefined ||
167
+ subjectIds.has(symbol.id) ||
168
+ !FALLING.has(symbol.kind) ||
169
+ classOf(symbol.id) !== "certain") {
136
170
  continue;
137
171
  }
138
- found.push(findingAbout(symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, namedBy, symbol, held), {
139
- writePositions: held.writes,
140
- }));
172
+ const innermost = chains.map((chain) => chain.find((id) => subjectIds.has(id)));
173
+ if (innermost.includes(undefined)) {
174
+ continue;
175
+ }
176
+ falls.add(symbol.id);
177
+ for (const subject of new Set(innermost)) {
178
+ const name = `${WORDS[symbol.kind]} ${symbol.name}`;
179
+ named.set(subject ?? "", [...(named.get(subject ?? "") ?? []), name]);
180
+ }
141
181
  }
142
- return found;
182
+ const written = { subjects, namedBy, named, falls };
183
+ WRITTEN.set(run.input, written);
184
+ return written;
185
+ }
186
+ /** `DS1301`: each subject, named with every position written and what falls with it. */
187
+ function writeOnlySymbols(run) {
188
+ const written = writtenOf(run);
189
+ return written.subjects.map((subject) => findingAbout(subject.symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, written, subject), {
190
+ writePositions: subject.held.writes,
191
+ }));
143
192
  }
144
193
  /**
145
194
  * The candidates no reference of any file names whose container is not itself dead, of
@@ -184,7 +233,14 @@ function unusedTypeParameters(run) {
184
233
  * supersedes narrowing it.
185
234
  */
186
235
  export function writeOnlyDeclarations(input) {
187
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
236
+ return new Set(writtenOf(indexed(input)).subjects.map(({ symbol }) => declarationKey(symbol.ref, symbol.position.path)));
237
+ }
238
+ /**
239
+ * The declarations that fall with a write-only subject, whatever the severity: a finding
240
+ * about one would name code its subject's deletion already deletes.
241
+ */
242
+ export function fallingWithWrites(input) {
243
+ return writtenOf(indexed(input)).falls;
188
244
  }
189
245
  /**
190
246
  * The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
@@ -1,5 +1,5 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
- import { testsUnseenAPI } from "./completion.js";
2
+ import { WORDS } from "../inventory.js";
3
3
  import { unreachableExports } from "./visibility-narrowing.js";
4
4
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
5
5
  const MEMBERS = new Set([
@@ -19,24 +19,6 @@ const NOT_REPORTED_HERE = new Set([
19
19
  "interface-method",
20
20
  "type-parameter",
21
21
  ]);
22
- /** The reader's word for each kind of declaration a finding of this emitter names. */
23
- const WORDS = {
24
- file: "file",
25
- function: "function",
26
- class: "class",
27
- interface: "interface",
28
- type: "type alias",
29
- enum: "enum",
30
- namespace: "namespace",
31
- variable: "variable",
32
- "export-alias": "re-export",
33
- method: "method",
34
- "class-member": "class member",
35
- "interface-method": "interface method",
36
- "type-member": "type member",
37
- "enum-member": "enum member",
38
- "type-parameter": "type parameter",
39
- };
40
22
  /** What the test-of-dead-code kind says, which states the rule it applies. */
41
23
  const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
42
24
  /**
@@ -50,7 +32,7 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
50
32
  function codeOf(judged) {
51
33
  const { candidate, symbol } = judged;
52
34
  if (candidate.testOfDeadCode) {
53
- return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
35
+ return CODE.testOfDeadCode;
54
36
  }
55
37
  if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
56
38
  return undefined;
@@ -201,7 +183,6 @@ function emitterFindings(input, fallen) {
201
183
  const union = input.swept.matrix.union;
202
184
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
203
185
  const unreachable = unreachableExports(input);
204
- const unseenAPI = testsUnseenAPI(input);
205
186
  let brands;
206
187
  const found = [];
207
188
  for (const candidate of input.swept.sweep.candidates) {
@@ -220,7 +201,6 @@ function emitterFindings(input, fallen) {
220
201
  test: union.test[at] === true,
221
202
  deprecated: input.deprecated.has(symbol.id),
222
203
  unreachable: unreachable.has(symbol.id),
223
- testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
224
204
  };
225
205
  const code = codeOf(judged);
226
206
  if (code === undefined ||
@@ -1,7 +1,9 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
- import { OUTSIDE } from "../graph.js";
2
+ import { OUTSIDE } from "../liveness/graph.js";
3
+ import { WORDS } from "../inventory.js";
3
4
  import { declarationKey } from "../ref.js";
4
- import { runnerCalls } from "../sweep.js";
5
+ import { runnerCalls } from "../liveness/sweep.js";
6
+ import { exposedTypes, exposes } from "../exposure.js";
5
7
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
6
8
  /**
7
9
  * The root kinds by which a consumer importing the package reaches a declaration. A
@@ -9,17 +11,6 @@ import { writeOnlyDeclarations } from "./reads-and-writes.js";
9
11
  * from outside the reference graph: a runtime, a tool, a framework or the configuration.
10
12
  */
11
13
  const PUBLISHING = new Set(["manifest-entry", "published-api"]);
12
- /** The reader's word for each kind of declaration this emitter reports. */
13
- const WORDS = {
14
- function: "function",
15
- class: "class",
16
- interface: "interface",
17
- type: "type alias",
18
- enum: "enum",
19
- namespace: "namespace",
20
- variable: "variable",
21
- "export-alias": "re-export",
22
- };
23
14
  function publishes(kinds) {
24
15
  return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
25
16
  }
@@ -102,9 +93,6 @@ function topLevelExport(facts, at, symbol) {
102
93
  const parent = facts.graph.parent[at] ?? OUTSIDE;
103
94
  return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
104
95
  }
105
- function wordOf(symbol) {
106
- return WORDS[symbol.kind] ?? symbol.kind;
107
- }
108
96
  /** The visibility each narrowing code names, which only the narrowing codes carry. */
109
97
  const NARROWER = {
110
98
  [CODE.redundantExportKeyword]: "file",
@@ -145,7 +133,7 @@ function narrowing(facts, at, symbol, world) {
145
133
  (facts.named[at] ?? 0) === 0) {
146
134
  return undefined;
147
135
  }
148
- const word = wordOf(symbol);
136
+ const word = WORDS[symbol.kind];
149
137
  switch (facts.reach[at]) {
150
138
  case "file":
151
139
  if (facts.published[file] === true && !closed) {
@@ -153,7 +141,7 @@ function narrowing(facts, at, symbol, world) {
153
141
  }
154
142
  return findingOf(symbol, CODE.redundantExportKeyword, `exported ${word} is referenced only inside the file that declares it`);
155
143
  case "package":
156
- if (!closed || !publishes(facts.rootedBy[at])) {
144
+ if (!closed || !publishes(facts.rootedBy[at]) || exposes(world.exposed, symbol)) {
157
145
  return undefined;
158
146
  }
159
147
  return findingOf(symbol, CODE.unnecessaryExport, `exported ${word} is referenced only inside the package that declares it`);
@@ -181,7 +169,7 @@ function unreachable(facts, at, symbol, candidate, input) {
181
169
  const found = candidate.relation === "reachability"
182
170
  ? "is referenced only from declarations that are themselves dead"
183
171
  : "has no reference in the target";
184
- return findingOf(symbol, CODE.unreachableExport, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
172
+ return findingOf(symbol, CODE.unreachableExport, `exported ${WORDS[symbol.kind]} ${found}, and nothing outside can import the file that declares it`);
185
173
  }
186
174
  /**
187
175
  * The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
@@ -194,7 +182,13 @@ export function narrowings(input) {
194
182
  const facts = factsOf(input.swept.matrix.union);
195
183
  const { declared, loaded } = input.boundary.consumers;
196
184
  const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
197
- const world = { closed, writeOnly: writeOnlyDeclarations(input) };
185
+ const world = {
186
+ closed,
187
+ writeOnly: writeOnlyDeclarations(input),
188
+ exposed: closed
189
+ ? exposedTypes(facts.graph, facts.reach.map((reach) => reach === "outside"), input.boundary.carriers)
190
+ : new Set(),
191
+ };
198
192
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
199
193
  const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
200
194
  const findings = [];
@@ -2,6 +2,24 @@ import { isClassDeclaration, isComputedPropertyName, isConstructorDeclaration, i
2
2
  import { byPosition, positionKey, renderPosition, startLine } from "./position.js";
3
3
  import { renderRef, spell } from "./ref.js";
4
4
  import { signatureOf } from "./signatures.js";
5
+ /** The reader's word for each kind of declaration, as a finding names it. */
6
+ export const WORDS = {
7
+ file: "file",
8
+ function: "function",
9
+ class: "class",
10
+ interface: "interface",
11
+ type: "type alias",
12
+ enum: "enum",
13
+ namespace: "namespace",
14
+ variable: "variable",
15
+ "export-alias": "re-export",
16
+ method: "method",
17
+ "class-member": "class member",
18
+ "interface-method": "interface method",
19
+ "type-member": "type member",
20
+ "enum-member": "enum member",
21
+ "type-parameter": "type parameter",
22
+ };
5
23
  /**
6
24
  * One node's identity inside one program, which is how two readings of that program name
7
25
  * one declaration: the binder's table and the tree meet here, and so does the reference
@@ -4,7 +4,7 @@
4
4
  * and every dead declaration that falls with them, being dead only through them.
5
5
  */
6
6
  import { OUTSIDE } from "./graph.js";
7
- import { PRODUCT } from "./product.js";
7
+ import { PRODUCT } from "../product.js";
8
8
  /** The fewest digits a component identifier's number is written with. */
9
9
  const ID_DIGITS = 4;
10
10
  /** The identifier of the component at one place of the run's order, counted from one. */
@@ -1,10 +1,10 @@
1
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.
2
+ * The run's declarations, the references every configuration makes between them and the
3
+ * roots that seed reachability, indexed for the sweep. The graph reads no file and asks the
4
+ * compiler nothing, so a sweep over it is a function of the values it was built from, and
5
+ * the inventory's order is the order every set a sweep answers reads in.
6
6
  */
7
- import { madeByTestCode } from "./tests/file-class.js";
7
+ import { madeByTestCode } from "../tests/file-class.js";
8
8
  /** The position of a declaration the inventory does not hold. */
9
9
  export const OUTSIDE = -1;
10
10
  /**
@@ -67,49 +67,63 @@ export function swept(kind) {
67
67
  export function namesACaller(kind) {
68
68
  return NAMES_A_CALLER[kind];
69
69
  }
70
+ /** Adds one reference's count to the counts of the declaration it names. */
71
+ function count(counts, test, consumer) {
72
+ if (test) {
73
+ counts.test += 1;
74
+ counts.consumerTest += consumer ? 1 : 0;
75
+ }
76
+ else {
77
+ counts.production += 1;
78
+ }
79
+ }
70
80
  /**
71
- * Indexes one project's inventory, references and roots. `testFiles` are the paths the
81
+ * Indexes the run's inventory, references and roots. `testFiles` are the paths the
72
82
  * reference pass classified as test files, so a declaration one holds is a test
73
83
  * declaration. A reference to a declaration the inventory does not hold is dropped; one
74
84
  * from such a declaration still counts toward its target, because reference counting
75
85
  * asks whether any declaration names the target. A root naming nothing is dropped.
76
86
  */
77
- export function graphOf(symbols, references, roots, testFiles, supportFiles = [], ambient = new Set()) {
87
+ export function graphOf(symbols, references, roots, testFiles, supportFiles = [], ambient = new Set(), sources = {}) {
78
88
  const supports = new Set(supportFiles);
79
89
  const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
80
90
  const at = (id) => index.get(id) ?? OUTSIDE;
81
91
  const tests = new Set(testFiles);
92
+ const unjudged = sources.unjudged ?? new Set();
82
93
  const out = symbols.map(() => []);
83
94
  const made = symbols.map(() => ({ production: 0, test: 0, consumerTest: 0 }));
95
+ const guessedMade = symbols.map(() => ({ production: 0, test: 0, consumerTest: 0 }));
84
96
  const consumed = [];
85
- for (const reference of references) {
97
+ references.forEach((reference, place) => {
86
98
  const to = at(reference.to);
87
99
  if (to === OUTSIDE) {
88
- continue;
100
+ return;
89
101
  }
90
102
  const test = madeByTestCode(reference, supports);
103
+ const consumer = reference.consumer !== undefined;
104
+ const guessed = reference.guessed === true;
91
105
  const counts = made[to];
92
106
  if (counts !== undefined) {
93
- if (test) {
94
- counts.test += 1;
95
- counts.consumerTest += reference.consumer === undefined ? 0 : 1;
96
- }
97
- else {
98
- counts.production += 1;
99
- }
107
+ count(counts, test, consumer);
108
+ }
109
+ const guesses = guessedMade[to];
110
+ if (guessed && guesses !== undefined) {
111
+ count(guesses, test, consumer);
100
112
  }
101
113
  const edge = {
102
114
  to,
115
+ reference: place,
103
116
  test,
117
+ guessed,
104
118
  evaluation: reference.use === "evaluation",
105
- consumer: reference.consumer !== undefined,
119
+ consumer,
106
120
  };
107
- if (edge.consumer || reference.unheld === true) {
121
+ if (consumer || reference.unheld === true || unjudged.has(reference.from)) {
108
122
  consumed.push(edge);
109
- continue;
123
+ return;
110
124
  }
111
125
  out[at(reference.from)]?.push(edge);
112
- }
126
+ });
113
127
  const parent = symbols.map((symbol) => at(symbol.parent));
114
128
  const exportsOf = symbols.map(() => []);
115
129
  symbols.forEach((symbol, position) => {
@@ -122,13 +136,16 @@ export function graphOf(symbols, references, roots, testFiles, supportFiles = []
122
136
  for (const root of roots) {
123
137
  const position = at(root.id);
124
138
  if (position !== OUTSIDE) {
125
- rooted.push({ at: position, kind: root.kind });
139
+ rooted.push({ at: position, kind: root.kind, source: root.source });
126
140
  }
127
141
  }
128
142
  return {
129
143
  symbols,
130
144
  out,
131
145
  made,
146
+ guessedMade,
147
+ references,
148
+ configurations: sources.configurations ?? references.map(() => []),
132
149
  parent,
133
150
  test: symbols.map((symbol) => tests.has(symbol.position.path)),
134
151
  support: symbols.map((symbol) => supports.has(symbol.position.path)),
@@ -0,0 +1,99 @@
1
+ /**
2
+ * One run's projects as one matrix: every declaration any project judges, keyed by its
3
+ * position and carrying the configurations that hold it, and one graph over every
4
+ * configuration's references and roots, which the sweep judges once (matrix.4, matrix.5).
5
+ */
6
+ import { graphOf, swept } from "./graph.js";
7
+ import { byPosition } from "../position.js";
8
+ /**
9
+ * What makes one reference one reference, whichever configuration made it. A consumer's
10
+ * position is rendered against its own root, so the consumer is part of what it names.
11
+ */
12
+ export function referenceKey(reference) {
13
+ const { path, line, column } = reference.position;
14
+ return [
15
+ reference.consumer ?? "",
16
+ reference.from,
17
+ reference.to,
18
+ path,
19
+ String(line),
20
+ String(column),
21
+ reference.use,
22
+ ].join("\u0000");
23
+ }
24
+ /**
25
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
26
+ * A declaration is keyed by its rendered position, with the fields of the first
27
+ * configuration that judges it; one in a file every holder holds only for its references
28
+ * is judged by none and is not in the union, and a reference it makes enters the union as a
29
+ * consumer's does. One reference at one position of a file several configurations compile
30
+ * is one reference; two one configuration makes are two.
31
+ */
32
+ export function matrixOf(per) {
33
+ const symbols = [];
34
+ const holders = new Map();
35
+ const references = [];
36
+ const makers = [];
37
+ const firstSeen = new Map();
38
+ const judged = new Set();
39
+ const unjudged = new Set();
40
+ const dependents = new Map();
41
+ per.forEach((one, index) => {
42
+ for (const [id, after] of one.dependents ?? []) {
43
+ if (!dependents.has(id)) {
44
+ dependents.set(id, after);
45
+ }
46
+ }
47
+ const referenceOnly = new Set(one.referenceOnly);
48
+ for (const symbol of one.symbols) {
49
+ const held = holders.get(symbol.id);
50
+ if (held === undefined) {
51
+ holders.set(symbol.id, [one.configuration]);
52
+ }
53
+ else if (!held.includes(one.configuration)) {
54
+ held.push(one.configuration);
55
+ }
56
+ if (referenceOnly.has(symbol.position.path)) {
57
+ if (swept(symbol.kind)) {
58
+ unjudged.add(symbol.id);
59
+ }
60
+ }
61
+ else if (!judged.has(symbol.id)) {
62
+ judged.add(symbol.id);
63
+ symbols.push(symbol);
64
+ }
65
+ }
66
+ for (const reference of one.references) {
67
+ const key = referenceKey(reference);
68
+ const seen = firstSeen.get(key);
69
+ if (seen !== undefined && seen.index !== index) {
70
+ for (const place of seen.places) {
71
+ const making = makers[place];
72
+ if (making !== undefined && !making.includes(one.configuration)) {
73
+ making.push(one.configuration);
74
+ }
75
+ }
76
+ continue;
77
+ }
78
+ const places = seen?.places ?? [];
79
+ if (seen === undefined) {
80
+ firstSeen.set(key, { index, places });
81
+ }
82
+ places.push(references.length);
83
+ references.push(reference);
84
+ makers.push([one.configuration]);
85
+ }
86
+ });
87
+ for (const id of judged) {
88
+ unjudged.delete(id);
89
+ }
90
+ symbols.sort((a, b) => byPosition(a.position, b.position));
91
+ const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
92
+ return {
93
+ configurations: per.map((one) => one.configuration),
94
+ union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])])), { unjudged, configurations: makers }),
95
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
96
+ testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
97
+ dependents,
98
+ };
99
+ }