@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.4

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 (137) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alias-chain.js +2 -2
  3. package/dist/src/analysis.js +89 -44
  4. package/dist/src/augmentations.js +14 -12
  5. package/dist/src/calls.js +2 -2
  6. package/dist/src/class-members.js +1 -1
  7. package/dist/src/components.js +62 -20
  8. package/dist/src/configuration-files.js +6 -6
  9. package/dist/src/configured-declarations.js +2 -2
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/contextual-uses.js +71 -14
  12. package/dist/src/convention-rows.js +6 -2
  13. package/dist/src/conventions.js +4 -2
  14. package/dist/src/dependencies.js +81 -27
  15. package/dist/src/deprecation.js +1 -1
  16. package/dist/src/destructuring.js +2 -2
  17. package/dist/src/discover.js +1 -1
  18. package/dist/src/element-access.js +341 -0
  19. package/dist/src/entry-points.js +2 -2
  20. package/dist/src/enum-group.js +2 -2
  21. package/dist/src/erased-stores.js +317 -0
  22. package/dist/src/findings/completion.js +46 -15
  23. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/self-check.js +17 -4
  26. package/dist/src/findings-pass.js +6 -5
  27. package/dist/src/framework-lifecycle.js +1 -1
  28. package/dist/src/html-entries.js +1 -1
  29. package/dist/src/implementations.js +2 -2
  30. package/dist/src/in-operands.js +30 -0
  31. package/dist/src/indexed-access.js +264 -125
  32. package/dist/src/injection-container.js +1 -1
  33. package/dist/src/interface-satisfaction.js +2 -2
  34. package/dist/src/intra-function-parts.js +26 -12
  35. package/dist/src/inventory.js +26 -1
  36. package/dist/src/keyof-comparisons.js +170 -0
  37. package/dist/src/kinds.js +17 -7
  38. package/dist/src/manifest.js +6 -16
  39. package/dist/src/matrix.js +20 -2
  40. package/dist/src/member-names.js +1 -1
  41. package/dist/src/module-declarations.js +1 -1
  42. package/dist/src/namespace-objects.js +1 -1
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/overrides.js +2 -2
  45. package/dist/src/query.js +4 -1
  46. package/dist/src/references.js +13 -2
  47. package/dist/src/reflective-lookup.js +2 -2
  48. package/dist/src/report.js +1 -1
  49. package/dist/src/reporters/sarif.js +4 -0
  50. package/dist/src/roots.js +1 -1
  51. package/dist/src/serialization-contract.js +45 -16
  52. package/dist/src/session.js +15 -8
  53. package/dist/src/signatures.js +1 -1
  54. package/dist/src/stores.js +1 -1
  55. package/dist/src/suppress-file.js +58 -6
  56. package/dist/src/suppress-inline.js +14 -5
  57. package/dist/src/suppress.js +54 -24
  58. package/dist/src/symbol-keys.js +1 -1
  59. package/dist/src/type-errors.js +1 -1
  60. package/dist/src/type-query-alias.js +1 -1
  61. package/dist/src/unimplemented-callees.js +312 -0
  62. package/dist/src/value-flow.js +1 -1
  63. package/dist/src/value-reach.js +8 -8
  64. package/dist/src/verbs/analyze.js +11 -9
  65. package/dist/src/workspace-programs.js +2 -2
  66. package/jsr.json +1 -1
  67. package/package.json +3 -3
  68. package/src/alias-chain.ts +2 -2
  69. package/src/analysis.ts +135 -52
  70. package/src/augmentations.ts +14 -11
  71. package/src/calls.ts +2 -2
  72. package/src/class-members.ts +2 -2
  73. package/src/component-layer.ts +1 -1
  74. package/src/components.ts +103 -21
  75. package/src/configuration-files.ts +6 -5
  76. package/src/configured-declarations.ts +2 -6
  77. package/src/conformance-record.ts +1 -1
  78. package/src/contextual-uses.ts +82 -18
  79. package/src/convention-rows.ts +6 -2
  80. package/src/conventions.ts +11 -2
  81. package/src/decorator.ts +1 -1
  82. package/src/dependencies.ts +108 -31
  83. package/src/deprecation.ts +1 -1
  84. package/src/destructuring.ts +2 -2
  85. package/src/discover.ts +2 -2
  86. package/src/element-access.ts +446 -0
  87. package/src/entry-points.ts +2 -2
  88. package/src/enum-group.ts +2 -2
  89. package/src/erased-stores.ts +413 -0
  90. package/src/file-facts.ts +3 -1
  91. package/src/findings/completion.ts +54 -20
  92. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  93. package/src/findings/emitters.ts +1 -1
  94. package/src/findings/self-check.ts +18 -4
  95. package/src/findings-pass.ts +12 -5
  96. package/src/framework-lifecycle.ts +2 -2
  97. package/src/html-entries.ts +1 -1
  98. package/src/implementations.ts +2 -2
  99. package/src/in-operands.ts +45 -0
  100. package/src/indexed-access.ts +304 -146
  101. package/src/injection-container.ts +1 -1
  102. package/src/interface-satisfaction.ts +2 -2
  103. package/src/intra-function-parts.ts +42 -17
  104. package/src/inventory.ts +34 -2
  105. package/src/keyof-comparisons.ts +211 -0
  106. package/src/kinds.ts +20 -7
  107. package/src/manifest.ts +6 -21
  108. package/src/matrix.ts +34 -2
  109. package/src/member-names.ts +1 -1
  110. package/src/module-declarations.ts +1 -1
  111. package/src/namespace-objects.ts +1 -1
  112. package/src/outside-tests.ts +1 -1
  113. package/src/overrides.ts +2 -2
  114. package/src/position.ts +1 -1
  115. package/src/query.ts +12 -2
  116. package/src/references.ts +25 -3
  117. package/src/reflective-lookup.ts +2 -2
  118. package/src/report.ts +3 -1
  119. package/src/reporters/sarif.ts +4 -0
  120. package/src/roots.ts +2 -2
  121. package/src/serialization-contract.ts +55 -16
  122. package/src/session.ts +39 -32
  123. package/src/signatures.ts +1 -1
  124. package/src/source-files.ts +1 -1
  125. package/src/stores.ts +1 -1
  126. package/src/suppress-file.ts +68 -5
  127. package/src/suppress-inline.ts +15 -5
  128. package/src/suppress.ts +63 -28
  129. package/src/symbol-keys.ts +2 -2
  130. package/src/type-errors.ts +2 -2
  131. package/src/type-query-alias.ts +1 -1
  132. package/src/unimplemented-callees.ts +411 -0
  133. package/src/value-flow.ts +1 -1
  134. package/src/value-reach.ts +12 -8
  135. package/src/verbs/analyze.ts +15 -9
  136. package/src/workflow-steps.ts +1 -1
  137. package/src/workspace-programs.ts +4 -4
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The members an element access reads by the literal type of its index: `o[k]` with `k`
3
+ * typed `"a" | "b"` reads `a` and `b` of the type of `o`, an index typed `keyof T` reads
4
+ * every member of `T`, and an index typed as a type parameter reads what its constraint does. Where the index is a parameter of a function every call of which the
5
+ * target holds, the members are read at each call, by the type of the argument it passes.
6
+ */
7
+ import { isArrayLiteralExpression, isArrowFunction, isBinaryExpression, isCallExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isFunctionDeclaration, isFunctionExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
8
+ import { nodeKey } from "./inventory.js";
9
+ import { renderPosition } from "./position.js";
10
+ import { UNANSWERED } from "./query.js";
11
+ /** The assignment operators that read their target before they write it. */
12
+ const COMPOUND = new Set([
13
+ SyntaxKind.PlusEqualsToken,
14
+ SyntaxKind.MinusEqualsToken,
15
+ SyntaxKind.AsteriskEqualsToken,
16
+ SyntaxKind.AsteriskAsteriskEqualsToken,
17
+ SyntaxKind.SlashEqualsToken,
18
+ SyntaxKind.PercentEqualsToken,
19
+ SyntaxKind.LessThanLessThanEqualsToken,
20
+ SyntaxKind.GreaterThanGreaterThanEqualsToken,
21
+ SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
22
+ SyntaxKind.AmpersandEqualsToken,
23
+ SyntaxKind.BarEqualsToken,
24
+ SyntaxKind.CaretEqualsToken,
25
+ SyntaxKind.AmpersandAmpersandEqualsToken,
26
+ SyntaxKind.BarBarEqualsToken,
27
+ SyntaxKind.QuestionQuestionEqualsToken,
28
+ ]);
29
+ /** How an expression is used where it is written: read, written, or both. */
30
+ function useOf(node) {
31
+ let at = node;
32
+ while (isParenthesizedExpression(at.parent)) {
33
+ at = at.parent;
34
+ }
35
+ const parent = at.parent;
36
+ if (isBinaryExpression(parent) && parent.left === at) {
37
+ if (parent.operatorToken.kind === SyntaxKind.EqualsToken) {
38
+ return "write";
39
+ }
40
+ if (COMPOUND.has(parent.operatorToken.kind)) {
41
+ return "both";
42
+ }
43
+ }
44
+ if ((isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
45
+ (parent.operator === SyntaxKind.PlusPlusToken || parent.operator === SyntaxKind.MinusMinusToken)) {
46
+ return "both";
47
+ }
48
+ return "read";
49
+ }
50
+ function unwrapped(node) {
51
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
52
+ }
53
+ /** Whether an index is a literal the checker resolves to a member by its own text. */
54
+ function isLiteralIndex(node) {
55
+ const at = unwrapped(node);
56
+ return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
57
+ }
58
+ /**
59
+ * The function `node` is, where every call of it is written in its own file under its own
60
+ * name: a function declaration or a `const` initialized with an arrow function or a function
61
+ * expression, neither exported. A method may be dispatched to through a base class or an
62
+ * interface, so it is none.
63
+ */
64
+ function callableOf(node) {
65
+ const exported = (holder) => (holder.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.ExportKeyword || modifier.kind === SyntaxKind.DefaultKeyword);
66
+ if (isFunctionDeclaration(node)) {
67
+ return node.name === undefined || exported(node)
68
+ ? undefined
69
+ : { name: node.name, parameters: node.parameters, body: node.body };
70
+ }
71
+ if (!isArrowFunction(node) && !isFunctionExpression(node)) {
72
+ return undefined;
73
+ }
74
+ const declaration = node.parent;
75
+ const list = declaration.parent;
76
+ if (!isVariableDeclaration(declaration) ||
77
+ declaration.initializer !== node ||
78
+ !isIdentifier(declaration.name) ||
79
+ !isVariableDeclarationList(list) ||
80
+ (list.flags & NodeFlags.Const) === 0 ||
81
+ !isVariableStatement(list.parent) ||
82
+ exported(list.parent)) {
83
+ return undefined;
84
+ }
85
+ return { name: declaration.name, parameters: node.parameters, body: node.body };
86
+ }
87
+ /** The innermost function enclosing a node, or none. */
88
+ function enclosingFunction(node) {
89
+ for (let at = node.parent; at !== undefined; at = at.parent) {
90
+ if (isFunctionDeclaration(at) ||
91
+ isArrowFunction(at) ||
92
+ isFunctionExpression(at) ||
93
+ at.kind === SyntaxKind.MethodDeclaration ||
94
+ at.kind === SyntaxKind.Constructor ||
95
+ at.kind === SyntaxKind.GetAccessor ||
96
+ at.kind === SyntaxKind.SetAccessor) {
97
+ return at;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /** Whether an assignment target, a destructuring pattern at any depth included, assigns a name. */
103
+ function targetAssigns(target, name) {
104
+ if (isParenthesizedExpression(target)) {
105
+ return targetAssigns(target.expression, name);
106
+ }
107
+ if (isIdentifier(target)) {
108
+ return target.text === name;
109
+ }
110
+ if (isArrayLiteralExpression(target)) {
111
+ return target.elements.some((element) => targetAssigns(element, name));
112
+ }
113
+ if (isObjectLiteralExpression(target)) {
114
+ return target.properties.some((property) => isPropertyAssignment(property)
115
+ ? targetAssigns(property.initializer, name)
116
+ : isShorthandPropertyAssignment(property)
117
+ ? isIdentifier(property.name) && property.name.text === name
118
+ : isSpreadAssignment(property) && targetAssigns(property.expression, name));
119
+ }
120
+ if (isSpreadElement(target)) {
121
+ return targetAssigns(target.expression, name);
122
+ }
123
+ // A pattern element with a default value, `[k = "a"] = …`, assigns its left side.
124
+ return (isBinaryExpression(target) &&
125
+ target.operatorToken.kind === SyntaxKind.EqualsToken &&
126
+ targetAssigns(target.left, name));
127
+ }
128
+ /**
129
+ * Whether a body assigns a name: by an assignment, a destructuring one included, an
130
+ * increment or a decrement, or as the target of a `for…in` or `for…of` loop.
131
+ */
132
+ function assigns(body, name) {
133
+ let found = false;
134
+ const visit = (node) => {
135
+ if (found) {
136
+ return;
137
+ }
138
+ const target = isBinaryExpression(node) &&
139
+ (node.operatorToken.kind === SyntaxKind.EqualsToken || COMPOUND.has(node.operatorToken.kind))
140
+ ? node.left
141
+ : (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) &&
142
+ (node.operator === SyntaxKind.PlusPlusToken ||
143
+ node.operator === SyntaxKind.MinusMinusToken)
144
+ ? node.operand
145
+ : (isForInStatement(node) || isForOfStatement(node)) &&
146
+ node.initializer.kind !== SyntaxKind.VariableDeclarationList
147
+ ? node.initializer
148
+ : undefined;
149
+ found = target !== undefined && targetAssigns(target, name);
150
+ node.forEachChild(visit);
151
+ };
152
+ if (body !== undefined) {
153
+ visit(body);
154
+ }
155
+ return found;
156
+ }
157
+ /**
158
+ * The references the element accesses of one project's own files make to the members their
159
+ * index's literal type names, each made by the declaration holding the access or the call.
160
+ */
161
+ export function elementAccessReferences(project, held, chains, root, testFiles) {
162
+ const queries = project.queries;
163
+ const candidates = [];
164
+ const calls = new Map();
165
+ const names = [];
166
+ for (const file of project.ownSourceFiles()) {
167
+ const fileId = held.declarations.get(nodeKey(file, file));
168
+ if (fileId === undefined) {
169
+ continue;
170
+ }
171
+ const visit = (node, from) => {
172
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
173
+ if (isElementAccessExpression(node) && !isLiteralIndex(node.argumentExpression)) {
174
+ candidates.push({ file, from: holder, access: node, use: useOf(node) });
175
+ }
176
+ if (isCallExpression(node)) {
177
+ calls.set(node, { file, from: holder });
178
+ }
179
+ if (isIdentifier(node)) {
180
+ names.push(node);
181
+ }
182
+ node.forEachChild((child) => {
183
+ visit(child, holder);
184
+ });
185
+ };
186
+ file.forEachChild((child) => {
187
+ visit(child, fileId);
188
+ });
189
+ }
190
+ if (candidates.length === 0) {
191
+ return [];
192
+ }
193
+ // A parameter index is decided at the calls only where the function is a callable every
194
+ // use of which is a call, so the symbols of the parameters and of every name are read.
195
+ const parameterOf = new Map();
196
+ const byCallable = new Map();
197
+ for (const candidate of candidates) {
198
+ const index = unwrapped(candidate.access.argumentExpression);
199
+ const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
200
+ const callable = fn === undefined ? undefined : callableOf(fn);
201
+ if (callable === undefined || !isIdentifier(index)) {
202
+ continue;
203
+ }
204
+ const at = callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === index.text);
205
+ if (at < 0 || assigns(callable.body, index.text)) {
206
+ continue;
207
+ }
208
+ parameterOf.set(candidate, { callable, at });
209
+ byCallable.set(callable.name, [...(byCallable.get(callable.name) ?? []), candidate]);
210
+ }
211
+ const callableNames = [...byCallable.keys()];
212
+ const spelled = new Set(callableNames.map((name) => name.text));
213
+ const sameName = names.filter((name) => spelled.has(name.text));
214
+ const symbols = queries.symbolsAt([...callableNames, ...sameName]);
215
+ const symbolOf = new Map();
216
+ [...callableNames, ...sameName].forEach((node, at) => {
217
+ const symbol = symbols[at];
218
+ if (symbol !== UNANSWERED && symbol !== undefined) {
219
+ symbolOf.set(node, symbol);
220
+ }
221
+ });
222
+ const callsOf = new Map();
223
+ for (const name of callableNames) {
224
+ const symbol = symbolOf.get(name);
225
+ let found = symbol === undefined ? undefined : [];
226
+ for (const use of sameName) {
227
+ if (found === undefined) {
228
+ break;
229
+ }
230
+ if (use === name || symbolOf.get(use) !== symbol) {
231
+ continue;
232
+ }
233
+ const parent = use.parent;
234
+ found =
235
+ isCallExpression(parent) && parent.expression === use ? [...found, parent] : undefined;
236
+ }
237
+ callsOf.set(name, found);
238
+ }
239
+ const lookups = [];
240
+ for (const candidate of candidates) {
241
+ const deferred = parameterOf.get(candidate);
242
+ const sites = deferred === undefined ? undefined : callsOf.get(deferred.callable.name);
243
+ if (deferred === undefined || sites === undefined) {
244
+ lookups.push({
245
+ file: candidate.file,
246
+ from: candidate.from,
247
+ at: candidate.access.argumentExpression,
248
+ object: candidate.access.expression,
249
+ index: candidate.access.argumentExpression,
250
+ use: candidate.use,
251
+ });
252
+ continue;
253
+ }
254
+ // An object that is a parameter too is read as the argument each call passes there.
255
+ const object = unwrapped(candidate.access.expression);
256
+ const objectAt = isIdentifier(object)
257
+ ? deferred.callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text)
258
+ : -1;
259
+ for (const call of sites) {
260
+ const parameter = deferred.callable.parameters[deferred.at];
261
+ const index = call.arguments[deferred.at] ?? parameter?.initializer;
262
+ const site = calls.get(call);
263
+ if (index === undefined || site === undefined) {
264
+ continue;
265
+ }
266
+ lookups.push({
267
+ file: site.file,
268
+ from: site.from,
269
+ at: call,
270
+ object: (objectAt < 0 ? undefined : call.arguments[objectAt]) ?? candidate.access.expression,
271
+ index,
272
+ use: candidate.use,
273
+ });
274
+ }
275
+ }
276
+ const nodes = lookups.flatMap((one) => [one.index, one.object]);
277
+ const types = queries.typesAt(nodes);
278
+ const typeOf = (node, at) => {
279
+ const type = types[at];
280
+ return nodes[at] === node && type !== UNANSWERED ? type : undefined;
281
+ };
282
+ // A type parameter's body serves every instantiation, so it reads what its constraint admits.
283
+ const keysOf = (type) => {
284
+ if (!type.isTypeParameter()) {
285
+ return type;
286
+ }
287
+ const constraint = queries.constraintOf(type);
288
+ return constraint === UNANSWERED || constraint === undefined ? type : constraint;
289
+ };
290
+ const partsOf = (type) => {
291
+ const parts = queries.constituents(type);
292
+ return parts === UNANSWERED ? [type] : parts;
293
+ };
294
+ const found = [];
295
+ lookups.forEach((lookup, at) => {
296
+ const indexType = typeOf(lookup.index, 2 * at);
297
+ const objectType = typeOf(lookup.object, 2 * at + 1);
298
+ const literals = indexType === undefined ? undefined : literalNames(partsOf(keysOf(indexType)));
299
+ if (objectType === undefined || literals === undefined) {
300
+ return;
301
+ }
302
+ const position = renderPosition(lookup.file, root, lookup.at.getStart());
303
+ for (const part of partsOf(objectType)) {
304
+ const properties = queries.propertiesOf(part);
305
+ for (const property of properties === UNANSWERED ? [] : properties) {
306
+ if (!literals.has(property.escapedName)) {
307
+ continue;
308
+ }
309
+ for (const to of chains.declarationsOf(property)) {
310
+ for (const use of lookup.use === "both" ? ["read", "write"] : [lookup.use]) {
311
+ found.push({
312
+ from: lookup.from,
313
+ to,
314
+ position,
315
+ use,
316
+ resolution: "keyed",
317
+ test: testFiles.has(position.path),
318
+ });
319
+ }
320
+ }
321
+ }
322
+ }
323
+ });
324
+ return found;
325
+ }
326
+ /** The names a union of literal types spells, or none where a constituent is no string or number literal. */
327
+ function literalNames(parts) {
328
+ const names = new Set();
329
+ for (const part of parts) {
330
+ if (part.isStringLiteralType()) {
331
+ names.add(part.value);
332
+ }
333
+ else if (part.isNumberLiteralType()) {
334
+ names.add(String(part.value));
335
+ }
336
+ else {
337
+ return undefined;
338
+ }
339
+ }
340
+ return names;
341
+ }
@@ -10,8 +10,8 @@
10
10
  * value that is not a literal is not evaluated and not guessed, so a file it would
11
11
  * have named is entered by nothing here.
12
12
  */
13
- import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
14
- import { SymbolFlags } from "@typescript/native/unstable/sync";
13
+ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/ast";
14
+ import { SymbolFlags } from "@typescript/native/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
16
  import { globExpression } from "./glob.js";
17
17
  import { htmlEntries } from "./html-entries.js";
@@ -6,8 +6,8 @@
6
6
  * or `unknown` expression asserted to it, or bound to a variable or returned from a
7
7
  * function it annotates, where a number counts too. An enum's own value converts nothing.
8
8
  */
9
- import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
10
- import { TypeFlags } from "@typescript/native/unstable/sync";
9
+ import { isArrowFunction, isAsExpression, isElementAccessExpression, isEnumDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isParenthesizedExpression, isParenthesizedTypeNode, isPropertyAccessExpression, isReturnStatement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
10
+ import { TypeFlags } from "@typescript/native/sync";
11
11
  import { aliasChains } from "./alias-chain.js";
12
12
  import { nodeKey } from "./inventory.js";
13
13
  import { renderPosition } from "./position.js";
@@ -0,0 +1,317 @@
1
+ /**
2
+ * The values the program stores in a slot of a container: a property, an index signature,
3
+ * an array element, and a variable. A serializer walking the container emits the value the
4
+ * slot holds, so it walks each stored value at its own static type, a subtype stored where
5
+ * the slot's type names a supertype, `object`, `unknown` or `any` included. A container is
6
+ * identified by the checker's type identity, so two containers of one type share what
7
+ * either stores. A store into an erased property of a type the analyzed program does not
8
+ * declare hands the value to the code that declares the type.
9
+ */
10
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
11
+ import { SymbolFlags, TypeFlags } from "@typescript/native/sync";
12
+ import { must } from "./query.js";
13
+ /** The types that keep nothing of the value they are given. */
14
+ const ERASED = TypeFlags.Any | TypeFlags.Unknown;
15
+ /** The array methods whose arguments become elements of the array they are called on. */
16
+ const ELEMENT_STORES = new Set(["push", "unshift"]);
17
+ /** The name an element access's literal index spells, or none. */
18
+ function literalKey(node) {
19
+ return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node) || isNumericLiteral(node)
20
+ ? node.text
21
+ : undefined;
22
+ }
23
+ function unwrapped(node) {
24
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
25
+ }
26
+ /**
27
+ * Whether a variable declaration's type can differ from what is stored in it: it carries an
28
+ * annotation, or it has neither an annotation nor an initializer, so its type evolves.
29
+ */
30
+ function declaresSlot(node) {
31
+ const held = node;
32
+ return held.type !== undefined || held.initializer === undefined;
33
+ }
34
+ /** The element of an array literal at one place, where no spread before it moves the places. */
35
+ function elementAt(source, at) {
36
+ if (!isArrayLiteralExpression(source)) {
37
+ return undefined;
38
+ }
39
+ const before = source.elements.slice(0, at + 1);
40
+ return before.some((one) => isSpreadElement(one)) ? undefined : source.elements[at];
41
+ }
42
+ /** The name a property spells, and none for a computed one. */
43
+ function propertyName(name) {
44
+ return isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)
45
+ ? name.text
46
+ : undefined;
47
+ }
48
+ /** The name one property of an object literal spells and the expression it holds, or none. */
49
+ function propertyParts(property) {
50
+ if (isPropertyAssignment(property)) {
51
+ const name = propertyName(property.name);
52
+ return name === undefined ? undefined : { name, value: property.initializer };
53
+ }
54
+ if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
55
+ return { name: property.name.text, value: property.name };
56
+ }
57
+ return undefined;
58
+ }
59
+ /** The value an object literal gives one property, where it gives it by a plain or a shorthand property. */
60
+ function propertyAt(source, name) {
61
+ if (!isObjectLiteralExpression(source)) {
62
+ return undefined;
63
+ }
64
+ for (const property of source.properties) {
65
+ const parts = propertyParts(property);
66
+ if (parts?.name === name) {
67
+ return parts.value;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Every target one destructuring pattern writes at any depth, each with the expression
74
+ * whose value it receives: the matching element or property of a literal on the right,
75
+ * else the right side with the path into it. A rest element copies, so it is no target
76
+ * here, and a default value is stored in its target too.
77
+ */
78
+ function patternTargets(pattern, from, out) {
79
+ const target = unwrapped(pattern);
80
+ const source = from.path.length === 0 ? unwrapped(from.value) : from.value;
81
+ const into = (element, step, literal) => {
82
+ patternTargets(element, literal === undefined
83
+ ? { value: from.value, path: [...from.path, step] }
84
+ : { value: literal, path: [] }, out);
85
+ };
86
+ if (isArrayLiteralExpression(target)) {
87
+ target.elements.forEach((element, at) => {
88
+ if (!isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression) {
89
+ into(element, at, from.path.length === 0 ? elementAt(source, at) : undefined);
90
+ }
91
+ });
92
+ return;
93
+ }
94
+ if (isObjectLiteralExpression(target)) {
95
+ for (const property of target.properties) {
96
+ const parts = propertyParts(property);
97
+ if (parts !== undefined) {
98
+ into(parts.value, parts.name, from.path.length === 0 ? propertyAt(source, parts.name) : undefined);
99
+ }
100
+ if (parts !== undefined &&
101
+ isShorthandPropertyAssignment(property) &&
102
+ property.objectAssignmentInitializer !== undefined) {
103
+ out.push({ target: parts.value, value: property.objectAssignmentInitializer, path: [] });
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ if (isBinaryExpression(target) && target.operatorToken.kind === SyntaxKind.EqualsToken) {
109
+ patternTargets(target.left, from, out);
110
+ out.push({ target: target.left, value: target.right, path: [] });
111
+ return;
112
+ }
113
+ out.push({ target, ...from });
114
+ }
115
+ /** Every store one project's own files write, and the stores into variables declared erased. */
116
+ export function erasedStores(project) {
117
+ const queries = project.queries;
118
+ const own = project.ownPaths();
119
+ const stores = [];
120
+ const assigned = [];
121
+ const slotVariables = [];
122
+ for (const file of project.ownSourceFiles()) {
123
+ const visit = (node) => {
124
+ if (isObjectLiteralExpression(node)) {
125
+ for (const property of node.properties) {
126
+ if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
127
+ const { name } = property;
128
+ stores.push({
129
+ container: node,
130
+ contextual: true,
131
+ name: name.text,
132
+ value: name,
133
+ path: [],
134
+ });
135
+ }
136
+ else if (isPropertyAssignment(property) &&
137
+ (isIdentifier(property.name) || isStringLiteral(property.name))) {
138
+ const value = property.initializer;
139
+ stores.push({
140
+ container: node,
141
+ contextual: true,
142
+ name: property.name.text,
143
+ value,
144
+ path: [],
145
+ });
146
+ }
147
+ }
148
+ }
149
+ else if (isArrayLiteralExpression(node)) {
150
+ for (const element of node.elements) {
151
+ if (!isSpreadElement(element)) {
152
+ stores.push({
153
+ container: node,
154
+ contextual: true,
155
+ name: undefined,
156
+ value: element,
157
+ path: [],
158
+ });
159
+ }
160
+ }
161
+ }
162
+ else if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.EqualsToken) {
163
+ const targets = [];
164
+ patternTargets(node.left, { value: node.right, path: [] }, targets);
165
+ for (const { target, value, path } of targets) {
166
+ if (isPropertyAccessExpression(target)) {
167
+ stores.push({
168
+ container: target.expression,
169
+ contextual: false,
170
+ name: target.name.text,
171
+ value,
172
+ path,
173
+ });
174
+ }
175
+ else if (isElementAccessExpression(target)) {
176
+ stores.push({
177
+ container: target.expression,
178
+ contextual: false,
179
+ name: literalKey(target.argumentExpression),
180
+ value,
181
+ path,
182
+ });
183
+ }
184
+ else if (isIdentifier(target)) {
185
+ assigned.push({ target, value, path });
186
+ }
187
+ }
188
+ }
189
+ else if (isCallExpression(node) &&
190
+ isPropertyAccessExpression(node.expression) &&
191
+ ELEMENT_STORES.has(node.expression.name.text)) {
192
+ for (const argument of node.arguments) {
193
+ stores.push({
194
+ container: node.expression.expression,
195
+ contextual: false,
196
+ name: undefined,
197
+ value: argument,
198
+ path: [],
199
+ });
200
+ }
201
+ }
202
+ else if (isVariableDeclaration(node) && declaresSlot(node)) {
203
+ slotVariables.push(node);
204
+ if (node.initializer !== undefined) {
205
+ assigned.push({ target: node, value: node.initializer, path: [] });
206
+ }
207
+ }
208
+ node.forEachChild(visit);
209
+ };
210
+ file.forEachChild(visit);
211
+ }
212
+ const contextual = stores.filter((one) => one.contextual);
213
+ const contexts = queries.contextualTypes(contextual.map((one) => one.container)).map(must);
214
+ const containerOf = new Map(contextual.map((one, at) => [one, contexts[at]]));
215
+ const typed = stores.filter((one) => !one.contextual);
216
+ queries
217
+ .typesAt(typed.map((one) => one.container))
218
+ .map(must)
219
+ .forEach((type, at) => {
220
+ const one = typed[at];
221
+ if (one !== undefined) {
222
+ containerOf.set(one, type);
223
+ }
224
+ });
225
+ const slotTypes = new Map();
226
+ /** The type a slot of one object type holds: a property's, else the string index's, else an element's. */
227
+ const slotOf = (type, name) => {
228
+ const key = `${String(type.id)}\u0000${name ?? ""}`;
229
+ if (slotTypes.has(key)) {
230
+ return slotTypes.get(key);
231
+ }
232
+ let found;
233
+ const property = name === undefined
234
+ ? undefined
235
+ : must(queries.propertiesOf(type)).find((one) => one.name === name);
236
+ if (property !== undefined) {
237
+ found =
238
+ (property.flags & SymbolFlags.Property) !== 0
239
+ ? must(queries.typeOfSymbol(property))
240
+ : undefined;
241
+ }
242
+ else if (type.isTypeReference() && must(queries.isArray(type))) {
243
+ found = must(queries.typeArguments(type))[0];
244
+ }
245
+ else {
246
+ found = must(queries.indexInfosOf(type))[0]?.valueType;
247
+ }
248
+ slotTypes.set(key, found);
249
+ return found;
250
+ };
251
+ const declaredOutside = (type) => {
252
+ const symbol = must(queries.symbolOfType(type));
253
+ return (symbol !== undefined &&
254
+ symbol.declarations.length > 0 &&
255
+ symbol.declarations.every((handle) => !own.has(handle.path)));
256
+ };
257
+ /** The type of the part of a stored value its path names, a step at a time. */
258
+ const partOf = (type, path) => path.reduce((at, step) => (at === undefined ? undefined : slotOf(at, String(step))), type);
259
+ const valueTypes = queries
260
+ .typesAt(stores.map((one) => one.value))
261
+ .map((type, at) => partOf(must(type), stores[at]?.path ?? []));
262
+ const inside = new Map();
263
+ const outside = [];
264
+ stores.forEach((store, at) => {
265
+ const container = containerOf.get(store);
266
+ const value = valueTypes[at];
267
+ if (container === undefined || value === undefined) {
268
+ return;
269
+ }
270
+ const parts = container.isUnionType() || container.isIntersectionType()
271
+ ? must(queries.constituents(container))
272
+ : [container];
273
+ for (const part of parts) {
274
+ if (!part.isObjectType()) {
275
+ continue;
276
+ }
277
+ const { name } = store;
278
+ const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
279
+ if (erased && declaredOutside(part)) {
280
+ outside.push({ value: store.value, type: value, property: name });
281
+ }
282
+ else {
283
+ inside.set(part.id, [...(inside.get(part.id) ?? []), value]);
284
+ }
285
+ }
286
+ });
287
+ const targets = assigned.map((one) => isVariableDeclaration(one.target) ? one.target.name : one.target);
288
+ const symbols = queries.symbolsAt(targets).map(must);
289
+ const slots = new Set(slotVariables);
290
+ const declarationOf = (symbol) => {
291
+ const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
292
+ return handle === undefined ? undefined : project.declarationAt(handle)?.node;
293
+ };
294
+ const assignedTypes = queries
295
+ .typesAt(assigned.map((one) => one.value))
296
+ .map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
297
+ const byVariable = new Map();
298
+ assigned.forEach((_one, at) => {
299
+ const declaration = declarationOf(symbols[at]);
300
+ const type = assignedTypes[at];
301
+ if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
302
+ byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
303
+ }
304
+ });
305
+ return {
306
+ inside: (type) => inside.get(type.id) ?? [],
307
+ ofVariable: (node) => {
308
+ if (byVariable.size === 0 || !isIdentifier(node)) {
309
+ return undefined;
310
+ }
311
+ const [symbol] = queries.symbolsAt([node]).map(must);
312
+ const declaration = declarationOf(symbol);
313
+ return declaration === undefined ? undefined : byVariable.get(declaration);
314
+ },
315
+ outside,
316
+ };
317
+ }