@cplieger/deadset-ts 5.2.3 → 6.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 (73) hide show
  1. package/dist/src/analysis.js +28 -5
  2. package/dist/src/augmentations.js +50 -0
  3. package/dist/src/components.js +15 -15
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/convention-rows.js +17 -2
  6. package/dist/src/conventions.js +82 -1
  7. package/dist/src/dependencies.js +42 -26
  8. package/dist/src/entry-points.js +28 -14
  9. package/dist/src/exempt.js +1 -0
  10. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  11. package/dist/src/findings/emitters.js +47 -12
  12. package/dist/src/findings/interfaces.js +4 -3
  13. package/dist/src/findings/intra-function.js +15 -2
  14. package/dist/src/findings/unused-declarations.js +13 -1
  15. package/dist/src/findings-pass.js +1 -0
  16. package/dist/src/graph.js +6 -6
  17. package/dist/src/implementations.js +1 -1
  18. package/dist/src/indexed-access.js +215 -0
  19. package/dist/src/interface-satisfaction.js +85 -23
  20. package/dist/src/intra-function-parts.js +139 -30
  21. package/dist/src/inventory.js +6 -1
  22. package/dist/src/kinds.js +13 -12
  23. package/dist/src/matrix.js +34 -9
  24. package/dist/src/module-declarations.js +180 -0
  25. package/dist/src/outside-tests.js +17 -15
  26. package/dist/src/overrides.js +70 -9
  27. package/dist/src/query.js +7 -0
  28. package/dist/src/ref.js +1 -0
  29. package/dist/src/references.js +6 -0
  30. package/dist/src/report.js +6 -1
  31. package/dist/src/reporters/sarif.js +3 -1
  32. package/dist/src/reporters/text.js +23 -3
  33. package/dist/src/roots.js +50 -10
  34. package/dist/src/version.js +1 -1
  35. package/dist/src/workflow-steps.js +82 -0
  36. package/jsr.json +1 -1
  37. package/package.json +7 -1
  38. package/src/analysis.ts +40 -4
  39. package/src/augmentations.ts +72 -0
  40. package/src/components.ts +15 -13
  41. package/src/conformance-record.ts +2 -2
  42. package/src/convention-rows.ts +25 -3
  43. package/src/conventions.ts +106 -2
  44. package/src/dependencies.ts +51 -31
  45. package/src/entry-points.ts +32 -19
  46. package/src/exempt.ts +4 -1
  47. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  48. package/src/findings/emitters.ts +71 -12
  49. package/src/findings/interfaces.ts +7 -6
  50. package/src/findings/intra-function.ts +20 -2
  51. package/src/findings/unused-declarations.ts +16 -1
  52. package/src/findings-pass.ts +4 -1
  53. package/src/graph.ts +6 -6
  54. package/src/implementations.ts +1 -1
  55. package/src/indexed-access.ts +295 -0
  56. package/src/interface-satisfaction.ts +118 -27
  57. package/src/intra-function-parts.ts +196 -35
  58. package/src/inventory.ts +8 -0
  59. package/src/kinds.ts +14 -12
  60. package/src/matrix.ts +51 -8
  61. package/src/module-declarations.ts +241 -0
  62. package/src/outside-tests.ts +18 -16
  63. package/src/overrides.ts +80 -14
  64. package/src/query.ts +28 -0
  65. package/src/ref.ts +1 -0
  66. package/src/references.ts +6 -0
  67. package/src/report.ts +11 -1
  68. package/src/reporters/sarif.ts +4 -1
  69. package/src/reporters/text.ts +25 -3
  70. package/src/roots.ts +56 -7
  71. package/src/sweep.ts +12 -0
  72. package/src/version.ts +1 -1
  73. package/src/workflow-steps.ts +104 -0
@@ -68,16 +68,40 @@ function dialsOver(input, found, suppressed, unmarked) {
68
68
  };
69
69
  }
70
70
  /**
71
- * Every finding of one run swept under its suppressions' marks, decided in one pass: every
72
- * family's findings completed by one step, so every component number is minted once, a
73
- * pending finding's among them; the finding each record claims withheld, whatever its
74
- * kind; the rest withheld by the dials, so a root one family reports withholds what
75
- * another family reports in its component; and every declared edge evaluated over what
76
- * remains, so a finding it holds pending is in its evaluation and nowhere else.
71
+ * Every finding of one run swept under its suppressions' marks, decided in one pass: each
72
+ * family's findings completed by one step, so every component number is minted once, the
73
+ * finding each record claims withheld, the rest withheld by the dials, so a root one family
74
+ * reports withholds what another reports in its component, and every declared edge
75
+ * evaluated over what remains, so a pending finding is in its evaluation alone. What the
76
+ * minimum confidence withheld is what the same decision at the lowest minimum adds.
77
77
  */
78
78
  export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
79
79
  const found = completedOver(input);
80
80
  const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
81
+ const would = [...unmarked, ...found];
82
+ const decided = dialledOver(input, suppressed, found, unmarked);
83
+ const shown = input.config.analysis.minConfidence === "possible"
84
+ ? decided
85
+ : dialledOver(atPossible(input), {
86
+ unmarked: atPossible(suppressed.unmarked),
87
+ records: suppressed.records,
88
+ }, found, unmarked);
89
+ return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
90
+ }
91
+ /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
92
+ function atPossible(input) {
93
+ const { config } = input;
94
+ return {
95
+ ...input,
96
+ config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
97
+ };
98
+ }
99
+ /**
100
+ * The run's completed findings decided under its dials: the finding each record claims
101
+ * withheld, the rest withheld by the dials, and every declared edge evaluated over what
102
+ * remains.
103
+ */
104
+ function dialledOver(input, suppressed, found, unmarked) {
81
105
  const would = [...unmarked, ...found];
82
106
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
83
107
  const unanswered = heldRefs(input);
@@ -87,12 +111,23 @@ export function decidedFindings(input, suppressed = { unmarked: input, records:
87
111
  !markup.has(finding.symbol.ref) &&
88
112
  !insideSkipped(input.swept.skipped, finding.position)));
89
113
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
90
- return {
91
- findings: evaluated.findings,
92
- evaluations: evaluated.evaluations,
93
- ledger,
94
- would,
95
- };
114
+ return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
115
+ }
116
+ /** The findings the lowest minimum would report that the run does not, counted by confidence. */
117
+ function withheldOf(reported, shown) {
118
+ const kept = new Set(reported.map(findingKey));
119
+ const counts = { certain: 0, probable: 0, possible: 0 };
120
+ for (const finding of shown) {
121
+ if (!kept.has(findingKey(finding))) {
122
+ counts[finding.confidence] += 1;
123
+ }
124
+ }
125
+ return counts;
126
+ }
127
+ /** A finding's identity across two decisions of one run: its position, code and subject. */
128
+ function findingKey(finding) {
129
+ const { path, line, column } = finding.position;
130
+ return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
96
131
  }
97
132
  /** Every finding the run reports, each family's in table order. */
98
133
  export function findingsOf(input) {
@@ -54,15 +54,16 @@ function findingOf(code, symbol, message, implementations) {
54
54
  * The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
55
55
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
56
56
  * interface no reference names, unless it is a marker, a method whose every
57
- * implementation has an empty body. A test file's declaration is neither kind's
58
- * subject, because a production sweep counts none of the references it can have.
57
+ * implementation has an empty body. A test file's declaration is neither kind's subject,
58
+ * because a production sweep counts none of the references it can have, save an interface
59
+ * the run that counts test references does not reach, which is `DS1201`'s.
59
60
  */
60
61
  export const interfaces = (input) => {
61
62
  const run = indexed(input);
62
63
  const found = [];
63
64
  for (const candidate of input.swept.sweep.candidates) {
64
65
  const symbol = run.symbolOf(candidate.id);
65
- if (symbol === undefined || run.inTest(candidate.id)) {
66
+ if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
66
67
  continue;
67
68
  }
68
69
  if (symbol.kind === "interface") {
@@ -14,6 +14,19 @@ function overlapClause(overlap) {
14
14
  ? "no other rule is known to report it"
15
15
  : `also reported by ${spelled(overlap)}`;
16
16
  }
17
+ /**
18
+ * Whether one parameter's name is reported: every unread one of a function whose
19
+ * signature is free, and of a function used as a value only one no read parameter follows,
20
+ * because a caller may pass such a function more arguments than it declares.
21
+ */
22
+ function parameterReported(part, free, freeButValued) {
23
+ const place = part.parameter;
24
+ if (place?.inValue !== true && free.has(part.declaration)) {
25
+ return true;
26
+ }
27
+ const valued = place?.inValue === true || freeButValued.has(part.declaration);
28
+ return valued && place?.beforeARead !== true;
29
+ }
17
30
  /**
18
31
  * Whether one part is reported. A parameter needs its function's signature free to
19
32
  * change; a result needs that, every call in the loaded program to discard it, and every
@@ -25,10 +38,10 @@ function reported(input, part, callersUnknown) {
25
38
  if (part.used) {
26
39
  return false;
27
40
  }
28
- const { free, discardedEverywhere } = input.intraFunction;
41
+ const { free, freeButValued, discardedEverywhere } = input.intraFunction;
29
42
  switch (part.code) {
30
43
  case UNUSED_PARAMETER:
31
- return free.has(part.declaration);
44
+ return parameterReported(part, free, freeButValued);
32
45
  case UNUSED_RESULT:
33
46
  return (free.has(part.declaration) &&
34
47
  discardedEverywhere.has(part.declaration) &&
@@ -49,9 +49,14 @@ function codeOf(judged) {
49
49
  if (candidate.testOfDeadCode) {
50
50
  return "DS1005";
51
51
  }
52
- if (judged.test || judged.containerDead || NOT_THIS_FAMILY.has(symbol.kind)) {
52
+ if ((judged.test && !candidate.unreferencedTest) ||
53
+ judged.containerDead ||
54
+ NOT_THIS_FAMILY.has(symbol.kind)) {
53
55
  return undefined;
54
56
  }
57
+ if (candidate.unreferencedTest) {
58
+ return unreferencedCode(symbol);
59
+ }
55
60
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
56
61
  if (symbol.kind === "enum-member" && !referenced) {
57
62
  return undefined;
@@ -73,6 +78,13 @@ function codeOf(judged) {
73
78
  }
74
79
  return judged.unreachable ? undefined : "DS1001";
75
80
  }
81
+ /** The code a declaration nothing references carries, by its shape and its visibility. */
82
+ function unreferencedCode(symbol) {
83
+ if (MEMBERS.has(symbol.kind)) {
84
+ return "DS1003";
85
+ }
86
+ return symbol.exported ? "DS1001" : "DS1002";
87
+ }
76
88
  /** The visibility a member's message names, where it narrows the member's reach. */
77
89
  function visibilityWord(symbol) {
78
90
  switch (symbol.visibility) {
@@ -55,6 +55,7 @@ export function findingsPass(inputs) {
55
55
  reasonsRecorded: counts.reasonsRecorded,
56
56
  staleSuppressions: records.length,
57
57
  pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
58
+ withheld: decided.withheld,
58
59
  },
59
60
  ledger,
60
61
  };
package/dist/src/graph.js CHANGED
@@ -32,11 +32,11 @@ const SWEPT = {
32
32
  };
33
33
  /**
34
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 and the files its strings name, a runtime runs a
36
- * test file, a setup file, a worker, a page's module script, a command and a script, the
37
- * manifest names what enters the package, an alias global stands for the export it names,
38
- * and an entry file, a configured root and a pattern assert a caller. A library's
39
- * published API only supposes an unseen consumer, so it is live under reachability alone.
35
+ * tool reads what it loads and what its strings name, a runtime runs a test, setup or
36
+ * worker file, a page's module script, a command, a script or a workflow step, the manifest
37
+ * names what enters the package, an alias global stands for its export, and an entry file,
38
+ * a configured root and a pattern assert a caller. A library's published API only supposes
39
+ * an unseen consumer, so it is live under reachability alone.
40
40
  */
41
41
  const NAMES_A_CALLER = {
42
42
  "entry-file": true,
@@ -52,10 +52,10 @@ const NAMES_A_CALLER = {
52
52
  "configuration-file": true,
53
53
  "configuration-string": true,
54
54
  "test-runner": true,
55
- "mutation-testing": true,
56
55
  "browser-tests": true,
57
56
  worker: true,
58
57
  "html-entry": true,
58
+ "workflow-step": true,
59
59
  };
60
60
  /** Whether the sweep judges a declaration of one kind. */
61
61
  export function swept(kind) {
@@ -54,7 +54,7 @@ function typesAt(project, nodes) {
54
54
  return typeAt;
55
55
  }
56
56
  /** Whether one class member is a method whose every written body holds no statement. */
57
- function emptyImplementation(project, member) {
57
+ export function emptyImplementation(project, member) {
58
58
  const own = project.ownPaths();
59
59
  let bodies = 0;
60
60
  for (const handle of member.declarations) {
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The members an indexed-access type names. `T["a" | "b"]` reads the members `a` and `b`
3
+ * of `T` as a property access by either name would. Where the index is a type parameter
4
+ * a call fixes, the call names the members the literal type argument names, written or
5
+ * inferred, which is read off the parameter the type parameter annotates whole. The type
6
+ * query of a type-query alias is no reference.
7
+ */
8
+ import { isArrowFunction, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isIndexedAccessTypeNode, isLiteralTypeNode, isMethodDeclaration, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedTypeNode, isPropertyAccessExpression, isStringLiteral, isTypeReferenceNode, isUnionTypeNode, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { renderPosition } from "./position.js";
11
+ import { UNANSWERED } from "./query.js";
12
+ import { typeQueryAliases } from "./type-query-alias.js";
13
+ function isGeneric(node) {
14
+ return ((isFunctionDeclaration(node) ||
15
+ isMethodDeclaration(node) ||
16
+ isArrowFunction(node) ||
17
+ isFunctionExpression(node)) &&
18
+ node.typeParameters !== undefined);
19
+ }
20
+ function unwrappedType(node) {
21
+ return isParenthesizedTypeNode(node) ? unwrappedType(node.type) : node;
22
+ }
23
+ /** The names a literal index type spells, or none where the index is any other type. */
24
+ function literalNames(index) {
25
+ const node = unwrappedType(index);
26
+ if (isUnionTypeNode(node)) {
27
+ const names = node.types.map(literalNames);
28
+ return names.every((one) => one !== undefined) ? names.flat() : undefined;
29
+ }
30
+ if (!isLiteralTypeNode(node)) {
31
+ return undefined;
32
+ }
33
+ const literal = node.literal;
34
+ if (isStringLiteral(literal) || isNoSubstitutionTemplateLiteral(literal)) {
35
+ return [literal.text];
36
+ }
37
+ return isNumericLiteral(literal) ? [String(Number(literal.text))] : undefined;
38
+ }
39
+ /** The name a literal type carries, a union's each, or none where a constituent is no literal. */
40
+ function literalTypeNames(type, parts) {
41
+ const names = [];
42
+ for (const part of parts(type)) {
43
+ if (part.isStringLiteralType()) {
44
+ names.push(part.value);
45
+ }
46
+ else if (part.isNumberLiteralType()) {
47
+ names.push(String(part.value));
48
+ }
49
+ else {
50
+ return undefined;
51
+ }
52
+ }
53
+ return names;
54
+ }
55
+ /**
56
+ * The references the indexed-access types of one project's own files make to the members
57
+ * they name, each made by the declaration that holds the type or the call, or by the file.
58
+ */
59
+ export function indexedAccessReferences(project, held, chains, root, testFiles) {
60
+ const queries = project.queries;
61
+ const accesses = [];
62
+ const generics = new Map();
63
+ const calls = [];
64
+ const names = new Set();
65
+ for (const file of project.ownSourceFiles()) {
66
+ const fileId = held.declarations.get(nodeKey(file, file));
67
+ if (fileId === undefined) {
68
+ continue;
69
+ }
70
+ const aliasTypes = new Set(typeQueryAliases(file).map((alias) => alias.type));
71
+ const visit = (node, from, enclosing) => {
72
+ const own = held.declarations.get(nodeKey(file, node));
73
+ const holder = own ?? from;
74
+ const inside = isGeneric(node) ? [...enclosing, node] : enclosing;
75
+ if (isIndexedAccessTypeNode(node) && !aliasTypes.has(node)) {
76
+ const literal = literalNames(node.indexType);
77
+ if (literal !== undefined) {
78
+ accesses.push({ file, from: holder, at: node, object: node.objectType, names: literal });
79
+ }
80
+ else {
81
+ parameterIndex(node, inside, file, generics, names);
82
+ }
83
+ }
84
+ if (isCallExpression(node) || isNewExpression(node)) {
85
+ const callee = node.expression;
86
+ const name = isIdentifier(callee)
87
+ ? callee.text
88
+ : isPropertyAccessExpression(callee) && isIdentifier(callee.name)
89
+ ? callee.name.text
90
+ : undefined;
91
+ if (name !== undefined) {
92
+ calls.push({ file, from: holder, call: node, name });
93
+ }
94
+ }
95
+ node.forEachChild((child) => {
96
+ visit(child, holder, inside);
97
+ });
98
+ };
99
+ file.forEachChild((child) => {
100
+ visit(child, fileId, []);
101
+ });
102
+ }
103
+ for (const { file, from, call, name } of calls) {
104
+ if (!names.has(name)) {
105
+ continue;
106
+ }
107
+ const resolved = queries.resolvedSignature(call);
108
+ if (resolved === UNANSWERED || resolved?.declaration === undefined) {
109
+ continue;
110
+ }
111
+ const declaration = project.declarationAt(resolved.declaration)?.node;
112
+ const byParameter = declaration === undefined
113
+ ? undefined
114
+ : generics.get(nodeKey(declaration.getSourceFile(), declaration));
115
+ if (byParameter === undefined) {
116
+ continue;
117
+ }
118
+ for (const { object, parameter } of byParameter) {
119
+ const fixed = queries.typeAtPosition(resolved, parameter);
120
+ const literal = fixed === UNANSWERED ? undefined : literalTypeNames(fixed, (type) => partsOf(type));
121
+ if (literal !== undefined) {
122
+ accesses.push({ file, from, at: call, object, names: literal });
123
+ }
124
+ }
125
+ }
126
+ function partsOf(type) {
127
+ const parts = queries.constituents(type);
128
+ return parts === UNANSWERED ? [type] : parts;
129
+ }
130
+ const objectNodes = accesses.flatMap((one) => ("kind" in one.object ? [one.object] : []));
131
+ const typeOf = new Map();
132
+ queries.typesAt(objectNodes).forEach((answer, index) => {
133
+ const node = objectNodes[index];
134
+ if (answer !== UNANSWERED && answer !== undefined && node !== undefined) {
135
+ typeOf.set(node, answer);
136
+ }
137
+ });
138
+ const found = [];
139
+ for (const access of accesses) {
140
+ const type = "kind" in access.object ? typeOf.get(access.object) : access.object;
141
+ if (type === undefined) {
142
+ continue;
143
+ }
144
+ const position = renderPosition(access.file, root, access.at.getStart());
145
+ for (const part of partsOf(type)) {
146
+ const properties = queries.propertiesOf(part);
147
+ if (properties === UNANSWERED) {
148
+ continue;
149
+ }
150
+ for (const property of properties) {
151
+ if (!access.names.includes(property.escapedName)) {
152
+ continue;
153
+ }
154
+ for (const to of chains.declarationsOf(property)) {
155
+ found.push({
156
+ from: access.from,
157
+ to,
158
+ position,
159
+ use: "read",
160
+ resolution: "batch",
161
+ test: testFiles.has(position.path),
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ return found;
168
+ }
169
+ /**
170
+ * Records an indexed-access type whose index names a type parameter of an enclosing
171
+ * function that a parameter of that function is annotated with whole, so a call's
172
+ * argument for that parameter fixes the index.
173
+ */
174
+ function parameterIndex(node, enclosing, file, generics, names) {
175
+ const index = unwrappedType(node.indexType);
176
+ if (!isTypeReferenceNode(index) || !isIdentifier(index.typeName)) {
177
+ return;
178
+ }
179
+ const spelled = index.typeName.text;
180
+ for (const generic of [...enclosing].reverse()) {
181
+ const declares = (generic.typeParameters ?? []).some((parameter) => isIdentifier(parameter.name) && parameter.name.text === spelled);
182
+ if (!declares) {
183
+ continue;
184
+ }
185
+ const at = generic.parameters.findIndex((parameter) => {
186
+ const type = parameter.type === undefined ? undefined : unwrappedType(parameter.type);
187
+ return (type !== undefined &&
188
+ isTypeReferenceNode(type) &&
189
+ isIdentifier(type.typeName) &&
190
+ type.typeName.text === spelled);
191
+ });
192
+ if (at >= 0) {
193
+ const key = nodeKey(file, generic);
194
+ generics.set(key, [...(generics.get(key) ?? []), { object: node.objectType, parameter: at }]);
195
+ const name = calledName(generic);
196
+ if (name !== undefined) {
197
+ names.add(name);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+ }
203
+ /** The name a call writes to call one generic function: its own, or its variable's. */
204
+ function calledName(generic) {
205
+ const own = generic.name;
206
+ if (own !== undefined && isIdentifier(own)) {
207
+ return own.text;
208
+ }
209
+ const parent = generic.parent;
210
+ return parent.kind === SyntaxKind.VariableDeclaration &&
211
+ parent.name !== undefined &&
212
+ isIdentifier(parent.name)
213
+ ? parent.name.text
214
+ : undefined;
215
+ }
@@ -1,20 +1,15 @@
1
1
  /**
2
- * The interface-satisfaction class: a class member is held back when a value of the
3
- * class reaches a position the checker types as an interface requiring a member of
4
- * that name, so a caller reaches the member through the interface while no name in
5
- * the program resolves to it.
6
- *
7
- * A value reaches such a position as an `implements` entry, the initializer of a
8
- * declaration whose type is written, an assignment, an argument, a returned value, an
9
- * array element, an object property, or the operand of `as` or `satisfies`, read
10
- * through parentheses, conditionals and the operators whose result is an operand.
11
- * `this` in an instance member is a value of its class. Types are asked in batches;
12
- * the contextual lookup has none, so it is asked only of a value whose type is a class
13
- * of the target, and each distinct pair, interface and class is asked once.
2
+ * The interface-satisfaction class: a member is held back when a value of its class
3
+ * reaches a position the checker types as an interface requiring that member, through an
4
+ * `implements` entry, a typed initializer, an assignment, an argument, a return, an array
5
+ * element, an object property or an `as` or `satisfies` operand. `this` is a value of its
6
+ * class, and a position typed by a type parameter reaches its constraint. The contextual
7
+ * lookup has no batch form, so it is asked only of a value typed as a target class.
14
8
  */
15
- import { isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isHeritageClause, isMethodDeclaration, isNewExpression, isObjectLiteralExpression, isSetAccessorDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
16
10
  import { SymbolFlags } from "@typescript/native/unstable/sync";
17
11
  import { aliasChains } from "./alias-chain.js";
12
+ import { emptyImplementation } from "./implementations.js";
18
13
  import { nodeKey } from "./inventory.js";
19
14
  import { renderPosition } from "./position.js";
20
15
  import { must } from "./query.js";
@@ -153,6 +148,10 @@ export function interfaceSatisfaction(input) {
153
148
  const interfaceNames = new Map();
154
149
  /** The interface types one position's type is or holds, each with the interface's name. */
155
150
  const interfacesOf = (type) => partsOf(type).flatMap((part) => {
151
+ if (part.isTypeParameter()) {
152
+ const constraint = must(queries.constraintOf(part));
153
+ return constraint === undefined ? [] : interfacesOf(constraint);
154
+ }
156
155
  if (!interfaceNames.has(part.id)) {
157
156
  const symbol = part.isObjectType() ? must(queries.symbolOfType(part)) : undefined;
158
157
  interfaceNames.set(part.id, symbol !== undefined && (symbol.flags & SymbolFlags.Interface) !== 0
@@ -162,6 +161,21 @@ export function interfaceSatisfaction(input) {
162
161
  const name = interfaceNames.get(part.id);
163
162
  return name === undefined ? [] : [{ type: part, name }];
164
163
  });
164
+ /**
165
+ * The type an argument's parameter declares in the generic signature a call
166
+ * instantiates, which is a type parameter where the argument is a type argument's value:
167
+ * the contextual type of such an argument is the instantiation, not the parameter.
168
+ */
169
+ const genericPosition = (value) => {
170
+ const call = value.parent;
171
+ if (!isCallExpression(call) && !isNewExpression(call)) {
172
+ return undefined;
173
+ }
174
+ const at = (call.arguments ?? []).findIndex((argument) => argument === value);
175
+ const resolved = at < 0 ? undefined : must(queries.resolvedSignature(call));
176
+ const generic = resolved === undefined ? undefined : must(queries.genericOf(resolved));
177
+ return generic === undefined ? undefined : must(queries.typeAtPosition(generic, at));
178
+ };
165
179
  const pairs = new Map();
166
180
  const reach = (owner, source, target, name, site) => {
167
181
  const key = `${String(source.id)}:${String(target.id)}`;
@@ -197,12 +211,11 @@ export function interfaceSatisfaction(input) {
197
211
  const positions = queries.contextualTypes(carrying.map((one) => one.flow.value));
198
212
  carrying.forEach(({ flow, sources }, index) => {
199
213
  const position = must(positions[index]);
200
- if (position === undefined) {
201
- return;
202
- }
203
- for (const reached of interfacesOf(position)) {
204
- for (const source of sources) {
205
- reach(source.owner, source.type, reached.type, reached.name, flow.value);
214
+ for (const type of [position, genericPosition(flow.value)]) {
215
+ for (const reached of type === undefined ? [] : interfacesOf(type)) {
216
+ for (const source of sources) {
217
+ reach(source.owner, source.type, reached.type, reached.name, flow.value);
218
+ }
206
219
  }
207
220
  }
208
221
  });
@@ -218,7 +231,10 @@ export function interfaceSatisfaction(input) {
218
231
  }
219
232
  let names = required.get(pair.target.id);
220
233
  if (names === undefined) {
221
- names = new Set(must(queries.propertiesOf(pair.target)).map((property) => property.escapedName));
234
+ names = new Map(must(queries.propertiesOf(pair.target)).map((property) => [
235
+ property.escapedName,
236
+ chains.declarationsOf(property),
237
+ ]));
222
238
  required.set(pair.target.id, names);
223
239
  }
224
240
  let answering = members.get(pair.owner.id);
@@ -226,18 +242,23 @@ export function interfaceSatisfaction(input) {
226
242
  const instance = instanceOf.get(pair.owner.id) ?? pair.source;
227
243
  answering = new Map(must(queries.propertiesOf(instance)).map((property) => [
228
244
  property.escapedName,
229
- chains.declarationsOf(property),
245
+ {
246
+ ids: chains.declarationsOf(property),
247
+ empty: emptyImplementation(project, property),
248
+ },
230
249
  ]));
231
250
  members.set(pair.owner.id, answering);
232
251
  }
233
252
  const detail = `satisfies ${pair.name}`;
234
- for (const name of names) {
235
- for (const id of answering.get(name) ?? []) {
253
+ for (const [name, methods] of names) {
254
+ const member = answering.get(name);
255
+ for (const id of member?.ids ?? []) {
236
256
  for (const site of pair.sites) {
237
257
  found.push({
238
258
  id,
239
259
  detail,
240
260
  site: renderPosition(site.getSourceFile(), targetRoot, site.getStart()),
261
+ implementing: { methods, empty: member?.empty ?? false },
241
262
  });
242
263
  }
243
264
  }
@@ -245,3 +266,44 @@ export function interfaceSatisfaction(input) {
245
266
  }
246
267
  return found;
247
268
  }
269
+ /**
270
+ * Each project's records with every record dropped whose member implements only interface
271
+ * methods nothing calls, unless every implementation of such a method is empty, and a
272
+ * reference from each such method to the member, which then falls with the method.
273
+ * `references` is every reference of the run, a consumer's included.
274
+ */
275
+ export function settledSatisfaction(projects, references) {
276
+ const methods = new Set(projects.flatMap((one) => one.held.symbols.filter((symbol) => symbol.kind === "interface-method").map((one) => one.id)));
277
+ const called = new Set(references.map((reference) => reference.to));
278
+ const bodies = new Map();
279
+ for (const record of projects.flatMap((one) => one.evidence)) {
280
+ for (const method of record.implementing?.methods ?? []) {
281
+ bodies.set(method, (bodies.get(method) ?? true) && record.implementing?.empty === true);
282
+ }
283
+ }
284
+ const uncalled = (method) => methods.has(method) && !called.has(method) && bodies.get(method) !== true;
285
+ return projects.map(({ evidence }) => {
286
+ const kept = [];
287
+ const added = [];
288
+ for (const record of evidence) {
289
+ const implemented = record.implementing?.methods ?? [];
290
+ if (record.class !== "interface-satisfaction" ||
291
+ implemented.length === 0 ||
292
+ !implemented.every(uncalled)) {
293
+ kept.push(record);
294
+ continue;
295
+ }
296
+ for (const method of implemented) {
297
+ added.push({
298
+ from: method,
299
+ to: record.id,
300
+ position: record.site,
301
+ use: "read",
302
+ resolution: "override",
303
+ test: false,
304
+ });
305
+ }
306
+ }
307
+ return { evidence: kept, references: added };
308
+ });
309
+ }