@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.21

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 (68) hide show
  1. package/dist/src/alias-chain.js +33 -2
  2. package/dist/src/analysis.js +18 -77
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/contract/contract.generated.js +4 -3
  5. package/dist/src/dependencies.js +1 -1
  6. package/dist/src/exemptions/argument-places.js +131 -0
  7. package/dist/src/{calls.js → exemptions/calls.js} +126 -179
  8. package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
  9. package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
  10. package/dist/src/exemptions/detectors.js +48 -0
  11. package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
  12. package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
  13. package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
  14. package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
  15. package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
  16. package/dist/src/exemptions/installed-globals.js +173 -0
  17. package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
  18. package/dist/src/exemptions/lifecycle.js +150 -0
  19. package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
  20. package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
  21. package/dist/src/exemptions/sites.js +182 -0
  22. package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
  23. package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
  24. package/dist/src/index/sites.js +1 -1
  25. package/dist/src/index/values.js +124 -0
  26. package/dist/src/liveness/sweep.js +2 -2
  27. package/dist/src/query.js +48 -0
  28. package/dist/src/roots/configured-declarations.js +1 -1
  29. package/dist/src/verbs/print-retained.js +23 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/alias-chain.ts +38 -2
  33. package/src/analysis.ts +19 -100
  34. package/src/bindings.ts +1 -1
  35. package/src/conformance-record.ts +2 -2
  36. package/src/contract/contract.generated.ts +4 -3
  37. package/src/dependencies.ts +1 -1
  38. package/src/exemptions/argument-places.ts +198 -0
  39. package/src/{calls.ts → exemptions/calls.ts} +190 -224
  40. package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
  41. package/src/exemptions/decorator.ts +56 -0
  42. package/src/exemptions/detectors.ts +75 -0
  43. package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
  44. package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
  45. package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
  46. package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
  47. package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
  48. package/src/exemptions/installed-globals.ts +255 -0
  49. package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
  50. package/src/exemptions/lifecycle.ts +286 -0
  51. package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
  52. package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
  53. package/src/exemptions/sites.ts +293 -0
  54. package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
  55. package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
  56. package/src/facts.ts +1 -1
  57. package/src/index/sites.ts +1 -1
  58. package/src/index/values.ts +170 -0
  59. package/src/liveness/sweep.ts +2 -2
  60. package/src/query.ts +109 -10
  61. package/src/roots/configured-declarations.ts +1 -2
  62. package/src/stores.ts +1 -1
  63. package/src/verbs/print-retained.ts +29 -1
  64. package/dist/src/exempt.js +0 -164
  65. package/dist/src/value-flow.js +0 -74
  66. package/src/decorator.ts +0 -78
  67. package/src/exempt.ts +0 -295
  68. package/src/value-flow.ts +0 -104
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The one walk of a project's own files every detector shares: its calls and the class
3
+ * `this` stands for at each, its classes, the values it hands to positions typed by
4
+ * their context, its functions' parameters, its decorated declarations and its
5
+ * assignments to properties. The walk reads syntax only; what a call invokes through a
6
+ * method of `Function.prototype` asks the checker, once per project, at the first
7
+ * detector that needs it.
8
+ */
9
+ import { isArrowFunction, isBinaryExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isElementAccessExpression, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyDeclaration, isSetAccessorDeclaration, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/ast";
10
+ import { handedOn, valuesOf } from "../index/values.js";
11
+ import { isParameterProperty, nodeKey } from "../inventory.js";
12
+ import { parenthesesRemoved } from "../unwrap.js";
13
+ import { decoratorsOf, invocations } from "./calls.js";
14
+ /** The property one assignment's left side names on its receiver, where a written key names it. */
15
+ function storeOf(node) {
16
+ if (!isBinaryExpression(node) || node.operatorToken.kind !== SyntaxKind.EqualsToken) {
17
+ return undefined;
18
+ }
19
+ const left = parenthesesRemoved(node.left);
20
+ if (isPropertyAccessExpression(left) && isIdentifier(left.name)) {
21
+ return { store: node, receiver: left.expression, key: left.name.text, value: node.right };
22
+ }
23
+ if (isElementAccessExpression(left) &&
24
+ (isStringLiteral(left.argumentExpression) ||
25
+ isNoSubstitutionTemplateLiteral(left.argumentExpression))) {
26
+ return {
27
+ store: node,
28
+ receiver: left.expression,
29
+ key: left.argumentExpression.text,
30
+ value: node.right,
31
+ };
32
+ }
33
+ return undefined;
34
+ }
35
+ /** Whether one node declares a static member, whose `this` is the class itself. */
36
+ function isStatic(node) {
37
+ const flags = node.modifierFlags ?? 0;
38
+ return (flags & ModifierFlags.Static) !== 0;
39
+ }
40
+ /**
41
+ * Whether one node opens a body whose `this` is no instance of the enclosing class: a
42
+ * function that is not an arrow, a static block, a static member, and a method or an
43
+ * accessor of an object literal, whose `this` is the literal.
44
+ */
45
+ function resetsThis(node) {
46
+ return (isFunctionDeclaration(node) ||
47
+ isFunctionExpression(node) ||
48
+ isClassStaticBlockDeclaration(node) ||
49
+ isStatic(node) ||
50
+ ((isMethodDeclaration(node) ||
51
+ isGetAccessorDeclaration(node) ||
52
+ isSetAccessorDeclaration(node)) &&
53
+ isObjectLiteralExpression(node.parent)));
54
+ }
55
+ /** The parameters and declaring node of one function-like declaration of the project. */
56
+ function functionOf(node) {
57
+ if (isFunctionDeclaration(node) ||
58
+ (isMethodDeclaration(node) && !isObjectLiteralExpression(node.parent))) {
59
+ return { declared: node, parameters: node.parameters };
60
+ }
61
+ if (isVariableDeclaration(node) &&
62
+ node.initializer !== undefined &&
63
+ (isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))) {
64
+ return { declared: node, parameters: node.initializer.parameters };
65
+ }
66
+ return undefined;
67
+ }
68
+ function walk(project, held) {
69
+ const calls = [];
70
+ const classes = [];
71
+ const flows = [];
72
+ const parameters = [];
73
+ const decorated = [];
74
+ const stores = [];
75
+ const parameterProperties = new Set();
76
+ const initialized = new Set();
77
+ for (const file of project.ownSourceFiles()) {
78
+ const visit = (node, self) => {
79
+ const id = held.declarations.get(nodeKey(file, node));
80
+ let inner = self;
81
+ if (isClassDeclaration(node) || isClassExpression(node)) {
82
+ inner = id;
83
+ if (id !== undefined) {
84
+ classes.push({ id, node });
85
+ }
86
+ }
87
+ else if (resetsThis(node)) {
88
+ inner = undefined;
89
+ }
90
+ if (id !== undefined) {
91
+ if (isParameterProperty(node)) {
92
+ parameterProperties.add(id);
93
+ }
94
+ if (decoratorsOf(node).length > 0) {
95
+ decorated.push({ id, node });
96
+ }
97
+ if (isPropertyDeclaration(node) && node.initializer !== undefined) {
98
+ initialized.add(id);
99
+ }
100
+ }
101
+ if (node.kind === SyntaxKind.CallExpression) {
102
+ calls.push({ call: node, self: inner });
103
+ }
104
+ const store = storeOf(node);
105
+ if (store !== undefined) {
106
+ stores.push(store);
107
+ }
108
+ for (const written of handedOn(node)) {
109
+ for (const value of valuesOf(written)) {
110
+ flows.push({ value, self: value.kind === SyntaxKind.ThisKeyword ? inner : undefined });
111
+ }
112
+ }
113
+ const fn = functionOf(node);
114
+ const fnId = fn === undefined ? undefined : held.declarations.get(nodeKey(file, fn.declared));
115
+ if (fn !== undefined && fnId !== undefined) {
116
+ fn.parameters
117
+ .filter((parameter) => !(isIdentifier(parameter.name) && parameter.name.text === "this"))
118
+ .forEach((parameter, index) => {
119
+ if (isIdentifier(parameter.name)) {
120
+ parameters.push({
121
+ fn: fnId,
122
+ index,
123
+ rest: parameter.dotDotDotToken !== undefined,
124
+ name: parameter.name,
125
+ });
126
+ }
127
+ });
128
+ }
129
+ node.forEachChild((child) => {
130
+ visit(child, inner);
131
+ });
132
+ };
133
+ file.forEachChild((child) => {
134
+ visit(child, undefined);
135
+ });
136
+ }
137
+ return {
138
+ calls,
139
+ classes,
140
+ flows,
141
+ parameters,
142
+ decorated,
143
+ stores,
144
+ parameterProperties,
145
+ initialized,
146
+ };
147
+ }
148
+ /** The sites of one project's own files, each walk and each checker question made once. */
149
+ export function projectSites(project, held) {
150
+ let walked;
151
+ let invoked;
152
+ const sites = () => (walked ??= walk(project, held));
153
+ return {
154
+ get calls() {
155
+ return sites().calls;
156
+ },
157
+ get classes() {
158
+ return sites().classes;
159
+ },
160
+ get flows() {
161
+ return sites().flows;
162
+ },
163
+ get parameters() {
164
+ return sites().parameters;
165
+ },
166
+ get decorated() {
167
+ return sites().decorated;
168
+ },
169
+ get stores() {
170
+ return sites().stores;
171
+ },
172
+ get parameterProperties() {
173
+ return sites().parameterProperties;
174
+ },
175
+ get initialized() {
176
+ return sites().initialized;
177
+ },
178
+ get invocations() {
179
+ return (invoked ??= invocations(project, sites().calls.map((one) => one.call)));
180
+ },
181
+ };
182
+ }
@@ -11,13 +11,13 @@
11
11
  * instance members of classes and the members of interfaces and object types, on any
12
12
  * type, because nothing ties the name to a type.
13
13
  */
14
- import { readComponent } from "./component-files.js";
15
- import { memberNames } from "./member-names.js";
16
- import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.js";
17
- import { isComponentFile } from "./position.js";
18
- import { bytewise } from "./text/compare.js";
19
- import { filesBelow } from "./fs/tree.js";
20
- import { UsageError } from "./usage.js";
14
+ import { readComponent } from "../component-files.js";
15
+ import { memberNames } from "../member-names.js";
16
+ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "../paths.js";
17
+ import { isComponentFile } from "../position.js";
18
+ import { bytewise } from "../text/compare.js";
19
+ import { filesBelow } from "../fs/tree.js";
20
+ import { UsageError } from "../usage.js";
21
21
  /** The kinds of declaration an action can name. */
22
22
  const READ_BY_A_TEMPLATE = new Set([
23
23
  "method",
@@ -2,14 +2,15 @@
2
2
  * The classes of the target a value of one type carries, and the members of the other
3
3
  * object types the target writes that it carries, as a serializer walking the value reaches
4
4
  * them: through constituents, array and tuple elements, the data members and index
5
- * signatures of the types the target writes, and the values `stored` gives for a type where
6
- * it erases them. The walk stops at `any`, `unknown`, a type parameter, a function and an
7
- * interface or class of another program, so an instance held only there is missed.
5
+ * signatures of the types the target writes, a class's polymorphic `this` type, and the values
6
+ * `stored` gives for a type where it erases them. The walk stops at `any`, `unknown`, any
7
+ * other type parameter, a function and an interface or class of another program, so an
8
+ * instance held only there is missed.
8
9
  */
9
10
  import { isAsExpression, isParenthesizedExpression, isSatisfiesExpression, isTypeAssertion, } from "@typescript/native/ast";
10
- import { ObjectFlags, SymbolFlags, } from "@typescript/native/sync";
11
- import { classNames, typesAt } from "./class-members.js";
12
- import { must } from "./query.js";
11
+ import { ObjectFlags, SymbolFlags, TypeFlags, } from "@typescript/native/sync";
12
+ import { classNames, typesAt } from "../class-members.js";
13
+ import { must } from "../query.js";
13
14
  /**
14
15
  * The object types whose members the walk reads whatever declares them. A reverse-mapped
15
16
  * type has no symbol: it is the shape the checker infers from an argument the target
@@ -19,7 +20,9 @@ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped | ObjectFlags.Rev
19
20
  /** The symbols of an anonymous object type the target writes as a type. */
20
21
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
21
22
  /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
22
- const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
23
+ export const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
24
+ /** The types that keep nothing of a value they are given, or that a decode gives a value. */
25
+ export const ERASED = TypeFlags.Any | TypeFlags.Unknown;
23
26
  /**
24
27
  * The members that can answer a conversion: a serializer reads the name with `[[Get]]`
25
28
  * and calls what it answers, so a getter answers it as a method does:
@@ -140,6 +143,10 @@ export function valueReach(project, held, stored = () => []) {
140
143
  if (type.isUnionType() || type.isIntersectionType()) {
141
144
  found = must(queries.constituents(type));
142
145
  }
146
+ else if (type.isTypeParameter() && type.isThisType === true) {
147
+ // A value of a class's polymorphic `this` type is an instance of the class.
148
+ found = [must(queries.constraintOf(type))].filter((one) => one !== undefined);
149
+ }
143
150
  else if (type.isObjectType()) {
144
151
  const target = targetOf(type);
145
152
  const isList = type.isTypeReference() &&
@@ -15,7 +15,7 @@ import { writtenName } from "../platform-calls.js";
15
15
  import { isLoadCall } from "../roots/unseen-loads.js";
16
16
  import { keyNameOf } from "../symbol-keys.js";
17
17
  import { parenthesesRemoved } from "../unwrap.js";
18
- import { handedOn, valuesOf } from "../value-flow.js";
18
+ import { handedOn, valuesOf } from "./values.js";
19
19
  /**
20
20
  * The operators whose left side one assignment stores into. A compound assignment is
21
21
  * among them: where its value is discarded it reads its target only to write the result
@@ -0,0 +1,124 @@
1
+ /**
2
+ * What a written expression evaluates to, the one definition every pass shares: the
3
+ * values it can be, the values a call's argument carries inside the literals it writes,
4
+ * and the positions a program hands a value to that the checker types from their context.
5
+ */
6
+ import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isIdentifier, isNewExpression, isNonNullExpression, isObjectLiteralExpression, isOmittedExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isPropertyDeclaration, isReturnStatement, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
7
+ /** The operators of an assignment whose right side is stored into the left side's type. */
8
+ const STORES = new Set([
9
+ SyntaxKind.EqualsToken,
10
+ SyntaxKind.QuestionQuestionEqualsToken,
11
+ SyntaxKind.BarBarEqualsToken,
12
+ SyntaxKind.AmpersandAmpersandEqualsToken,
13
+ ]);
14
+ /** Whether a declaration writes its type, so its initializer is read in that type's context. */
15
+ function writesType(node) {
16
+ return node.type !== undefined;
17
+ }
18
+ /**
19
+ * The expressions one written value can evaluate to, each read alone because the checker
20
+ * reduces a union by subtype, so the type at the whole drops a class another branch's
21
+ * extends: the operand of a spread, of parentheses and of `!`, each branch of a
22
+ * conditional, each operand of `??` and `||`, and the right operand of `&&` and of a
23
+ * comma. The left operand of `&&` is its value only where it is falsy, which no object
24
+ * is. An assertion is a value of its own, whose type its reader decides.
25
+ */
26
+ export function valuesOf(expression) {
27
+ if (isSpreadElement(expression) || isParenthesizedExpression(expression)) {
28
+ return valuesOf(expression.expression);
29
+ }
30
+ if (isNonNullExpression(expression)) {
31
+ return valuesOf(expression.expression);
32
+ }
33
+ if (isConditionalExpression(expression)) {
34
+ return [...valuesOf(expression.whenTrue), ...valuesOf(expression.whenFalse)];
35
+ }
36
+ if (isBinaryExpression(expression)) {
37
+ const operator = expression.operatorToken.kind;
38
+ if (operator === SyntaxKind.QuestionQuestionToken || operator === SyntaxKind.BarBarToken) {
39
+ return [...valuesOf(expression.left), ...valuesOf(expression.right)];
40
+ }
41
+ if (operator === SyntaxKind.AmpersandAmpersandToken || operator === SyntaxKind.CommaToken) {
42
+ return valuesOf(expression.right);
43
+ }
44
+ }
45
+ return [expression];
46
+ }
47
+ /** The name node a callee, a decorator expression or a value resolves through, where it has one. */
48
+ export function nameOf(expression) {
49
+ if (isParenthesizedExpression(expression)) {
50
+ return nameOf(expression.expression);
51
+ }
52
+ if (isPropertyAccessExpression(expression)) {
53
+ return expression.name;
54
+ }
55
+ return isIdentifier(expression) ? expression : undefined;
56
+ }
57
+ /**
58
+ * The values one argument carries: each value it can be ({@link valuesOf}), or what an
59
+ * array or object literal it can be holds, at any depth, through spreads and shorthand
60
+ * properties. Each value is read inside every assertion around it.
61
+ */
62
+ export function carriedValues(argument) {
63
+ return valuesOf(argument).flatMap((value) => {
64
+ let at = value;
65
+ while (isAsExpression(at) || isSatisfiesExpression(at)) {
66
+ at = at.expression;
67
+ }
68
+ if (at !== value) {
69
+ return carriedValues(at);
70
+ }
71
+ if (isArrayLiteralExpression(value)) {
72
+ return value.elements.flatMap(carriedValues);
73
+ }
74
+ if (isObjectLiteralExpression(value)) {
75
+ return value.properties.flatMap((property) => {
76
+ if (isPropertyAssignment(property)) {
77
+ return carriedValues(property.initializer);
78
+ }
79
+ if (isSpreadAssignment(property)) {
80
+ return carriedValues(property.expression);
81
+ }
82
+ return isShorthandPropertyAssignment(property)
83
+ ? [{ site: property.name, name: property, shorthand: true }]
84
+ : [];
85
+ });
86
+ }
87
+ return isOmittedExpression(value)
88
+ ? []
89
+ : [{ site: value, name: nameOf(value), shorthand: false }];
90
+ });
91
+ }
92
+ /** The expressions one node hands to a position typed by its context. */
93
+ export function handedOn(node) {
94
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node) || isParameterDeclaration(node)) &&
95
+ node.initializer !== undefined &&
96
+ writesType(node)) {
97
+ return [node.initializer];
98
+ }
99
+ if (isBinaryExpression(node) && STORES.has(node.operatorToken.kind)) {
100
+ return [node.right];
101
+ }
102
+ if (isCallExpression(node) || isNewExpression(node)) {
103
+ return (node.arguments ?? []).filter((argument) => !isSpreadElement(argument));
104
+ }
105
+ if (isReturnStatement(node)) {
106
+ return node.expression === undefined ? [] : [node.expression];
107
+ }
108
+ if (isArrowFunction(node)) {
109
+ return node.body.kind === SyntaxKind.Block ? [] : [node.body];
110
+ }
111
+ if (isArrayLiteralExpression(node)) {
112
+ return node.elements.filter((element) => !isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression);
113
+ }
114
+ if (isPropertyAssignment(node)) {
115
+ return [node.initializer];
116
+ }
117
+ if (isShorthandPropertyAssignment(node)) {
118
+ return isIdentifier(node.name) ? [node.name] : [];
119
+ }
120
+ if (isAsExpression(node) || isSatisfiesExpression(node)) {
121
+ return [node.expression];
122
+ }
123
+ return [];
124
+ }
@@ -6,7 +6,7 @@
6
6
  * first: a caller, an exemption or a live component file's markup, a suppression mark, and an
7
7
  * unanswered checker question; each Contract rule "live only through" one is a view up to it.
8
8
  */
9
- import { exemptionsOf, isUnansweredRecord } from "../exempt.js";
9
+ import { exemptionsOf } from "../exemptions/lifecycle.js";
10
10
  import { edgeWithheldBy } from "../tests/file-class.js";
11
11
  import { componentsOf } from "./components.js";
12
12
  import { namesACaller, OUTSIDE } from "./graph.js";
@@ -367,7 +367,7 @@ function seedsOf(graph, policy) {
367
367
  if (record.whileLive !== undefined) {
368
368
  gate(record.whileLive, seed);
369
369
  }
370
- else if (isUnansweredRecord(record)) {
370
+ else if (record.unanswered === true) {
371
371
  unanswered.push(seed);
372
372
  }
373
373
  else {
package/dist/src/query.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * form where it has one and otherwise as one batch, one round trip per capped run.
8
8
  */
9
9
  import { isIdentifier } from "@typescript/native/ast";
10
+ import { IndexKind, } from "@typescript/native/sync";
10
11
  /**
11
12
  * The greatest number of locations one batched request asks about. A batch is
12
13
  * answered by an array of its own length, so an uncapped batch over a large file
@@ -118,6 +119,41 @@ function partQuestion(type) {
118
119
  }
119
120
  return undefined;
120
121
  }
122
+ /** How deep {@link mappedParameters} reads, as a recursive mapped type makes a new one at each level. */
123
+ const MAPPED_DEPTH = 4;
124
+ /** The type parameters a mapped type is over, as {@link Queries.mappedParametersOf} names them. */
125
+ function mappedParameters(checker, mapped) {
126
+ const own = new Set();
127
+ const found = new Map();
128
+ const seen = new Set();
129
+ const visit = (type, depth) => {
130
+ if (depth > MAPPED_DEPTH || seen.has(type.id)) {
131
+ return;
132
+ }
133
+ seen.add(type.id);
134
+ if (type.isMappedType()) {
135
+ own.add(type.getTypeParameter().id);
136
+ }
137
+ if (type.isTypeParameter() && !type.isThisType) {
138
+ found.set(type.id, type);
139
+ }
140
+ const aliased = type
141
+ .aliasTypeArguments;
142
+ const inner = [
143
+ ...(type.isUnionType() || type.isIntersectionType() ? type.getTypes() : []),
144
+ ...(type.isTypeReference() ? checker.getTypeArguments(type) : []),
145
+ ...(aliased === undefined || aliased.length === 0 ? [] : type.getAliasTypeArguments()),
146
+ ...(partQuestion(type)?.() ?? []),
147
+ ];
148
+ for (const part of inner) {
149
+ if (part !== undefined) {
150
+ visit(part, depth + 1);
151
+ }
152
+ }
153
+ };
154
+ visit(mapped, 0);
155
+ return [...found.values()].filter((parameter) => !own.has(parameter.id));
156
+ }
121
157
  /**
122
158
  * The guarded questions over one project's checker. `configFile` names the project
123
159
  * in the log, `batch` sends many questions in one round trip, `log` is the run's,
@@ -244,6 +280,7 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
244
280
  constraintOf: (type) => type.isTypeParameter()
245
281
  ? one("getConstraint", () => typeLocation(type), () => type.getConstraint())
246
282
  : undefined,
283
+ baseConstraintOf: (type) => one("getBaseConstraintOfType", () => typeLocation(type), () => checker.getBaseConstraintOfType(type)),
247
284
  indexedAccessOf: (type) => type.isIndexedAccessType()
248
285
  ? one("getObjectType", () => typeLocation(type), () => ({ object: type.getObjectType(), index: type.getIndexType() }))
249
286
  : undefined,
@@ -272,6 +309,17 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
272
309
  ? undefined
273
310
  : one("getTarget", () => `signature ${String(signature.id)}`, () => signature.getTarget()),
274
311
  typeAtPosition: (signature, index) => one("getTypeParameterAtPosition", () => `signature ${String(signature.id)} position ${String(index)}`, () => signature.getTypeParameterAtPosition(index)),
312
+ restTypeOf: (signature) => signature.hasRestParameter
313
+ ? one("getTypeOfSymbol", () => `signature ${String(signature.id)} rest parameter`, () => {
314
+ const rest = signature.getParameters().at(-1);
315
+ return rest === undefined ? undefined : checker.getTypeOfSymbol(rest);
316
+ })
317
+ : undefined,
318
+ numberIndexOf: (type) => one("getIndexTypeOfType", () => typeLocation(type), () => checker.getIndexTypeOfType(type, IndexKind.Number)),
319
+ mappedParametersOf: (type) => type.isMappedType()
320
+ ? one("getConstraintType", () => typeLocation(type), () => mappedParameters(checker, type))
321
+ : [],
322
+ propertyTypeOf: (type, name) => one("getTypeOfPropertyOfType", () => `${typeLocation(type)} property ${name}`, () => checker.getTypeOfPropertyOfType(type, name)),
275
323
  signatureOf: (declaration) => one("getSignatureFromDeclaration", () => nodeLocation(declaration), () => checker.getSignatureFromDeclaration(declaration)),
276
324
  returnTypeOf: (signature) => one("getReturnTypeOfSignature", () => `signature ${String(signature.id)}`, () => checker.getReturnTypeOfSignature(signature)),
277
325
  typeToString: (type) => one("typeToString", () => typeLocation(type), () => checker.typeToString(type)),
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
8
8
  import { SymbolFlags } from "@typescript/native/sync";
9
- import { resolvedTargets } from "../calls.js";
9
+ import { resolvedTargets } from "../alias-chain.js";
10
10
  import { declaredComponent } from "../inventory.js";
11
11
  import { must, Unanswerable } from "../query.js";
12
12
  import { nameComponent } from "../ref.js";
@@ -1,11 +1,33 @@
1
1
  import { EXIT_CLEAN } from "../contract/contract.generated.js";
2
2
  import { productionMode, runSweep } from "../analysis.js";
3
- import { retainedLines } from "../exempt.js";
4
3
  import { partialNotes } from "../partial.js";
4
+ import { positionKey } from "../position.js";
5
5
  import { resolvePath } from "../paths.js";
6
6
  import { resolve } from "../resolve.js";
7
7
  import {} from "./verb.js";
8
8
  import { diagnose } from "../product.js";
9
+ /**
10
+ * One line per held-back declaration, in site order: its reference and position, then
11
+ * each record's class, site and detail, all separated by tabs.
12
+ */
13
+ function retainedLines(symbols, retained) {
14
+ const held = new Map();
15
+ for (const record of retained) {
16
+ held.set(record.id, [...(held.get(record.id) ?? []), record]);
17
+ }
18
+ return symbols.flatMap((symbol) => {
19
+ const records = held.get(symbol.id);
20
+ if (records === undefined) {
21
+ return [];
22
+ }
23
+ const fields = records.flatMap((record) => [
24
+ record.class,
25
+ positionKey(record.site),
26
+ record.detail,
27
+ ]);
28
+ return [[symbol.ref, positionKey(symbol.position), ...fields].join("\t")];
29
+ });
30
+ }
9
31
  /**
10
32
  * Writes every declaration an exemption held back in the production sweep a report is
11
33
  * built from, one per line, and nothing where no exemption held one back. One fact
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.20",
3
+ "version": "8.0.0-dev.21",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.20",
3
+ "version": "8.0.0-dev.21",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Where an import or export chain leads in one project: from one symbol to each
3
3
  * declaration of the target it is, and along the chain of aliases it heads to each
4
- * declaration every link of that chain is.
4
+ * declaration every link of that chain is, and what a name resolves to through them.
5
5
  */
6
6
 
7
7
  import {
@@ -12,10 +12,11 @@ import {
12
12
  isImportSpecifier,
13
13
  isNamespaceExport,
14
14
  isNamespaceImport,
15
+ type Node,
15
16
  } from "@typescript/native/ast";
16
17
  import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
17
18
  import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
18
- import { UNANSWERED, type Answer } from "./query.ts";
19
+ import { must, UNANSWERED, type Answer } from "./query.ts";
19
20
  import type { ProjectView } from "./session.ts";
20
21
 
21
22
  /** One declaration a chain reaches, and whether a step along an alias reached it. */
@@ -195,3 +196,38 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
195
196
  nextDeclared,
196
197
  };
197
198
  }
199
+
200
+ /**
201
+ * What each of a batch of nodes resolves to, every alias followed to the declaration
202
+ * it stands for. A node the checker resolves to nothing, and an alias that resolves
203
+ * to nothing, are absent from the answer. Each alias is followed once. A question the
204
+ * checker does not answer stops the pass, as {@link must} stops.
205
+ */
206
+ export function resolvedTargets<Brand>(
207
+ project: ProjectView<Brand>,
208
+ nodes: readonly Node[],
209
+ ): ReadonlyMap<Node, TSSymbol> {
210
+ const followed = new Map<number, TSSymbol | undefined>();
211
+ const targets = new Map<Node, TSSymbol>();
212
+ project
213
+ .symbolsAt(nodes.map((node) => project.handle(node)))
214
+ .map(must)
215
+ .forEach((symbol, index) => {
216
+ const node = nodes[index];
217
+ if (symbol === undefined || node === undefined) {
218
+ return;
219
+ }
220
+ let target: TSSymbol | undefined = symbol;
221
+ if ((symbol.flags & SymbolFlags.Alias) !== 0) {
222
+ if (!followed.has(symbol.id)) {
223
+ const aliased = must(project.queries.aliased(symbol));
224
+ followed.set(symbol.id, aliased.declarations.length === 0 ? undefined : aliased);
225
+ }
226
+ target = followed.get(symbol.id);
227
+ }
228
+ if (target !== undefined) {
229
+ targets.set(node, target);
230
+ }
231
+ });
232
+ return targets;
233
+ }