@cplieger/deadset-ts 7.0.1 → 7.1.0-dev.2
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.
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/erased-stores.js +22 -19
- package/dist/src/serialization-contract.js +101 -22
- package/dist/src/value-reach.js +31 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/conformance-record.ts +2 -2
- package/src/erased-stores.ts +29 -19
- package/src/serialization-contract.ts +116 -26
- package/src/value-reach.ts +43 -0
- package/src/version.ts +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "5.
|
|
5
|
+
corpusVersion: "5.1.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:befd5e68f8d68e859b2a0920464a5470a157d5d40a881d9cca28716a4575751d",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The values the program stores in a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
6
|
-
* identified by the checker's type identity, so two containers of one type share what
|
|
2
|
+
* The values the program stores in a property, an index signature, an array element or a
|
|
3
|
+
* variable. A serializer emits the value a slot holds, so it walks each stored value at its
|
|
4
|
+
* own static type and each type it is checked against, whatever supertype the slot names.
|
|
5
|
+
* Containers are identified by the checker's type identity, so two of one type share what
|
|
7
6
|
* 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
|
|
7
|
+
* declare hands the value to the code that declares it.
|
|
9
8
|
*/
|
|
10
9
|
import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
|
|
11
10
|
import { SymbolFlags, TypeFlags } from "@typescript/native/sync";
|
|
12
11
|
import { must } from "./query.js";
|
|
12
|
+
import { checkedTypes } from "./value-reach.js";
|
|
13
13
|
/** The types that keep nothing of the value they are given. */
|
|
14
14
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
15
15
|
/** The array methods whose arguments become elements of the array they are called on. */
|
|
@@ -256,15 +256,20 @@ export function erasedStores(project) {
|
|
|
256
256
|
};
|
|
257
257
|
/** The type of the part of a stored value its path names, a step at a time. */
|
|
258
258
|
const partOf = (type, path) => path.reduce((at, step) => (at === undefined ? undefined : slotOf(at, String(step))), type);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.map((
|
|
259
|
+
/** The types of what each stored value puts in its slot: its own and those it is checked against. */
|
|
260
|
+
const storedTypes = (stored) => {
|
|
261
|
+
const checked = checkedTypes(project, stored.map((one) => one.value));
|
|
262
|
+
return queries
|
|
263
|
+
.typesAt(stored.map((one) => one.value))
|
|
264
|
+
.map((type, at) => [must(type), ...(checked[at] ?? [])].flatMap((one) => partOf(one, stored[at]?.path ?? []) ?? []));
|
|
265
|
+
};
|
|
266
|
+
const valueTypes = storedTypes(stores);
|
|
262
267
|
const inside = new Map();
|
|
263
268
|
const outside = [];
|
|
264
269
|
stores.forEach((store, at) => {
|
|
265
270
|
const container = containerOf.get(store);
|
|
266
|
-
const
|
|
267
|
-
if (container === undefined
|
|
271
|
+
const values = valueTypes[at] ?? [];
|
|
272
|
+
if (container === undefined) {
|
|
268
273
|
return;
|
|
269
274
|
}
|
|
270
275
|
const parts = container.isUnionType() || container.isIntersectionType()
|
|
@@ -277,10 +282,10 @@ export function erasedStores(project) {
|
|
|
277
282
|
const { name } = store;
|
|
278
283
|
const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
|
|
279
284
|
if (erased && declaredOutside(part)) {
|
|
280
|
-
outside.push({ value: store.value, type
|
|
285
|
+
outside.push(...values.map((type) => ({ value: store.value, type, property: name })));
|
|
281
286
|
}
|
|
282
287
|
else {
|
|
283
|
-
inside.set(part.id, [...(inside.get(part.id) ?? []),
|
|
288
|
+
inside.set(part.id, [...(inside.get(part.id) ?? []), ...values]);
|
|
284
289
|
}
|
|
285
290
|
}
|
|
286
291
|
});
|
|
@@ -291,15 +296,13 @@ export function erasedStores(project) {
|
|
|
291
296
|
const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
|
|
292
297
|
return handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
293
298
|
};
|
|
294
|
-
const assignedTypes =
|
|
295
|
-
.typesAt(assigned.map((one) => one.value))
|
|
296
|
-
.map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
|
|
299
|
+
const assignedTypes = storedTypes(assigned);
|
|
297
300
|
const byVariable = new Map();
|
|
298
301
|
assigned.forEach((_one, at) => {
|
|
299
302
|
const declaration = declarationOf(symbols[at]);
|
|
300
|
-
const
|
|
301
|
-
if (declaration !== undefined && slots.has(declaration) &&
|
|
302
|
-
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []),
|
|
303
|
+
const types = assignedTypes[at] ?? [];
|
|
304
|
+
if (declaration !== undefined && slots.has(declaration) && types.length > 0) {
|
|
305
|
+
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), ...types]);
|
|
303
306
|
}
|
|
304
307
|
});
|
|
305
308
|
return {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* function of the project handing such a parameter to a destination is one, to a fixpoint,
|
|
7
7
|
* and a function invoked through `call`, `apply` or `bind` is the callee.
|
|
8
8
|
*/
|
|
9
|
-
import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
9
|
+
import { isArrayLiteralExpression, isArrowFunction, isAsExpression, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isInterfaceDeclaration, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isTypeLiteralNode, isVariableDeclaration, ModifierFlags, NodeFlags, SyntaxKind, } from "@typescript/native/ast";
|
|
10
10
|
import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/sync";
|
|
11
11
|
import { aliasChains } from "./alias-chain.js";
|
|
12
12
|
import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
|
|
@@ -16,7 +16,7 @@ import { nodeKey } from "./inventory.js";
|
|
|
16
16
|
import { renderPosition } from "./position.js";
|
|
17
17
|
import { must } from "./query.js";
|
|
18
18
|
import { erasedStores } from "./erased-stores.js";
|
|
19
|
-
import { valueReach } from "./value-reach.js";
|
|
19
|
+
import { checkedTypes, valueReach } from "./value-reach.js";
|
|
20
20
|
/** The serializer every run reads, whatever the configuration names. */
|
|
21
21
|
const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
|
|
22
22
|
/** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
|
|
@@ -217,13 +217,74 @@ function handedOn(project, sites, erased, invocationOf) {
|
|
|
217
217
|
return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
|
|
218
218
|
});
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* The symbols of the interfaces and object types code of the analysis implements: those a
|
|
222
|
+
* class's `implements` and `extends` clauses name, those an object literal is typed as by
|
|
223
|
+
* a variable or property annotation or a `satisfies` or `as` expression, and every type
|
|
224
|
+
* each of those extends. Read at the first call, which asks the checker.
|
|
225
|
+
*/
|
|
226
|
+
function implementedOwners(project, inAnalysis) {
|
|
227
|
+
let owners;
|
|
228
|
+
return () => {
|
|
229
|
+
if (owners !== undefined) {
|
|
230
|
+
return owners;
|
|
231
|
+
}
|
|
232
|
+
const found = new Set();
|
|
233
|
+
const queries = project.queries;
|
|
234
|
+
const own = project.ownPaths();
|
|
235
|
+
const files = [
|
|
236
|
+
...project.ownSourceFiles(),
|
|
237
|
+
...project.program
|
|
238
|
+
.getSourceFileNames()
|
|
239
|
+
.filter((name) => !own.has(name) && inAnalysis(name))
|
|
240
|
+
.flatMap((name) => project.program.getSourceFile(name) ?? []),
|
|
241
|
+
];
|
|
242
|
+
const naming = [];
|
|
243
|
+
const visit = (node) => {
|
|
244
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
245
|
+
naming.push(...(node.heritageClauses ?? []).flatMap((clause) => [...clause.types]));
|
|
246
|
+
}
|
|
247
|
+
else if ((isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
248
|
+
node.type !== undefined &&
|
|
249
|
+
node.initializer !== undefined &&
|
|
250
|
+
isObjectLiteralExpression(evaluated(node.initializer))) {
|
|
251
|
+
naming.push(node.type);
|
|
252
|
+
}
|
|
253
|
+
else if ((isSatisfiesExpression(node) || isAsExpression(node)) &&
|
|
254
|
+
isObjectLiteralExpression(evaluated(node.expression))) {
|
|
255
|
+
naming.push(node.type);
|
|
256
|
+
}
|
|
257
|
+
node.forEachChild(visit);
|
|
258
|
+
};
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
file.forEachChild(visit);
|
|
261
|
+
}
|
|
262
|
+
const pending = typesAt(project, naming).filter((type) => type !== undefined);
|
|
263
|
+
const seen = new Set();
|
|
264
|
+
for (let type = pending.pop(); type !== undefined; type = pending.pop()) {
|
|
265
|
+
for (const part of must(queries.constituents(type))) {
|
|
266
|
+
const target = must(queries.targetOf(part));
|
|
267
|
+
if (!seen.has(target.id)) {
|
|
268
|
+
seen.add(target.id);
|
|
269
|
+
const symbol = must(queries.symbolOfType(target));
|
|
270
|
+
if (symbol !== undefined) {
|
|
271
|
+
found.add(symbol.id);
|
|
272
|
+
}
|
|
273
|
+
pending.push(...must(queries.baseTypes(target)));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
owners = found;
|
|
278
|
+
return owners;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
220
281
|
/**
|
|
221
282
|
* The destinations of one project's calls. A call is one at every argument where its
|
|
222
283
|
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
223
|
-
* `unknown` or `any` where its callee is declared outside the analysis
|
|
224
|
-
* callee is a function of the project, at each
|
|
225
|
-
* destination, holding back the widest of what those
|
|
226
|
-
* settled by widening until no parameter moves.
|
|
284
|
+
* `unknown` or `any` where its callee is declared outside the analysis or only ambiently
|
|
285
|
+
* with no implementation in it; and, where its callee is a function of the project, at each
|
|
286
|
+
* parameter the function hands on to a destination, holding back the widest of what those
|
|
287
|
+
* destinations hold back, which is settled by widening until no parameter moves.
|
|
227
288
|
*/
|
|
228
289
|
function destinations(input, chains, walked) {
|
|
229
290
|
const { project, held } = input;
|
|
@@ -249,9 +310,29 @@ function destinations(input, chains, walked) {
|
|
|
249
310
|
for (const parameter of erased.values()) {
|
|
250
311
|
byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
|
|
251
312
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
313
|
+
const inAnalysis = (path) => own.has(path) || input.consumers.some((root) => path === root || path.startsWith(`${root}/`));
|
|
314
|
+
const implemented = implementedOwners(project, inAnalysis);
|
|
315
|
+
/**
|
|
316
|
+
* Whether every declaration of a callee is outside the analysis or ambient. An ambient
|
|
317
|
+
* declaration has no body, so the code that runs is outside the analysis, unless it is a
|
|
318
|
+
* member of an interface or an object type a class or a typed object literal of the
|
|
319
|
+
* analysis implements; `settled` false skips that question, which asks the checker.
|
|
320
|
+
*/
|
|
321
|
+
const outside = (target, settled) => target.declarations.length > 0 &&
|
|
322
|
+
target.declarations.every((handle) => {
|
|
323
|
+
if (!inAnalysis(handle.path)) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
const node = project.declarationAt(handle)?.node;
|
|
327
|
+
if (node === undefined || (node.flags & NodeFlags.Ambient) === 0) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
if (!settled || !(isInterfaceDeclaration(node.parent) || isTypeLiteralNode(node.parent))) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
const owner = must(queries.parentOf(target));
|
|
334
|
+
return owner === undefined || !implemented().has(owner.id);
|
|
335
|
+
});
|
|
255
336
|
const invocation = (call) => {
|
|
256
337
|
const form = forms.get(call);
|
|
257
338
|
const method = form === undefined ? undefined : callees.get(form.method);
|
|
@@ -259,7 +340,7 @@ function destinations(input, chains, walked) {
|
|
|
259
340
|
// program's own `call` and `Reflect.apply` invoke nothing here.
|
|
260
341
|
if (form !== undefined &&
|
|
261
342
|
method !== undefined &&
|
|
262
|
-
outside(method) &&
|
|
343
|
+
outside(method, false) &&
|
|
263
344
|
FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")) {
|
|
264
345
|
return form.invocation;
|
|
265
346
|
}
|
|
@@ -303,16 +384,15 @@ function destinations(input, chains, walked) {
|
|
|
303
384
|
if (entry !== undefined) {
|
|
304
385
|
return { reach: "properties", callee: spelledEntry(entry.entry, held) };
|
|
305
386
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return outside(target) && erasedAt(call, argument)
|
|
387
|
+
if (outside(target, true)) {
|
|
388
|
+
return erasedAt(call, argument)
|
|
309
389
|
? {
|
|
310
390
|
reach: "conversions",
|
|
311
391
|
callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
|
|
312
392
|
}
|
|
313
393
|
: undefined;
|
|
314
394
|
}
|
|
315
|
-
for (const fn of
|
|
395
|
+
for (const fn of chains.declarationsOf(target)) {
|
|
316
396
|
const parameter = (byFunction.get(fn) ?? []).find((one) => one.index === argument || (one.rest && one.index <= argument));
|
|
317
397
|
const reach = parameter === undefined ? undefined : forwards.get(keyOf(parameter));
|
|
318
398
|
if (reach !== undefined) {
|
|
@@ -339,14 +419,12 @@ function destinations(input, chains, walked) {
|
|
|
339
419
|
if (target === undefined) {
|
|
340
420
|
return false;
|
|
341
421
|
}
|
|
342
|
-
|
|
343
|
-
if (named.some((one) => names(one, target, chains))) {
|
|
422
|
+
if (named.some((one) => names(one, target, chains)) || outside(target, false)) {
|
|
344
423
|
return true;
|
|
345
424
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return declared.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
425
|
+
return chains
|
|
426
|
+
.declarationsOf(target)
|
|
427
|
+
.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
350
428
|
};
|
|
351
429
|
return { invocation, at, mayReceive };
|
|
352
430
|
}
|
|
@@ -366,7 +444,8 @@ function flowsOf(project, held, sites, to, stores) {
|
|
|
366
444
|
const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
|
|
367
445
|
const types = typesAt(project, asked.map((one) => one.value));
|
|
368
446
|
const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
|
|
369
|
-
const
|
|
447
|
+
const checked = checkedTypes(project, arguments_.map((one) => one.value));
|
|
448
|
+
const passed = arguments_.flatMap((one, index) => {
|
|
370
449
|
const type = typeOf.get(one.value);
|
|
371
450
|
let reached = { classes: new Set(), members: [] };
|
|
372
451
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
@@ -378,7 +457,7 @@ function flowsOf(project, held, sites, to, stores) {
|
|
|
378
457
|
else if (type !== undefined) {
|
|
379
458
|
reached = reach.reachedFrom(type);
|
|
380
459
|
}
|
|
381
|
-
for (const stored of stores.ofVariable(one.value) ?? []) {
|
|
460
|
+
for (const stored of [...(checked[index] ?? []), ...(stores.ofVariable(one.value) ?? [])]) {
|
|
382
461
|
reached = joined(reached, reach.reachedFrom(stored));
|
|
383
462
|
}
|
|
384
463
|
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
package/dist/src/value-reach.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* it erases them. The walk stops at `any`, `unknown`, a type parameter, a function and an
|
|
7
7
|
* interface or class of another program, so an instance held only there is missed.
|
|
8
8
|
*/
|
|
9
|
+
import { isAsExpression, isParenthesizedExpression, isSatisfiesExpression, isTypeAssertion, } from "@typescript/native/ast";
|
|
9
10
|
import { ObjectFlags, SymbolFlags, } from "@typescript/native/sync";
|
|
10
11
|
import { classNames, typesAt } from "./class-members.js";
|
|
11
12
|
import { must } from "./query.js";
|
|
@@ -15,6 +16,36 @@ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
|
|
|
15
16
|
const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
|
|
16
17
|
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
17
18
|
const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
|
|
19
|
+
/**
|
|
20
|
+
* The types each value is checked against by the `satisfies` expressions and type assertions
|
|
21
|
+
* it is written as, through parentheses, and the innermost operand's own type where there is
|
|
22
|
+
* one. A `satisfies` expression's static type is its operand's, so an object literal it
|
|
23
|
+
* checks carries its own members and not the target's, though the literal's properties
|
|
24
|
+
* write the target's; an assertion to `unknown` or `any` hides the operand's type.
|
|
25
|
+
*/
|
|
26
|
+
export function checkedTypes(project, values) {
|
|
27
|
+
const nodes = values.map((value) => {
|
|
28
|
+
const found = [];
|
|
29
|
+
let at = value;
|
|
30
|
+
while (isParenthesizedExpression(at) ||
|
|
31
|
+
isSatisfiesExpression(at) ||
|
|
32
|
+
isAsExpression(at) ||
|
|
33
|
+
isTypeAssertion(at)) {
|
|
34
|
+
if (!isParenthesizedExpression(at)) {
|
|
35
|
+
found.push(at.type);
|
|
36
|
+
}
|
|
37
|
+
at = at.expression;
|
|
38
|
+
}
|
|
39
|
+
return found.length === 0 ? found : [...found, at];
|
|
40
|
+
});
|
|
41
|
+
const types = typesAt(project, nodes.flat());
|
|
42
|
+
let next = 0;
|
|
43
|
+
return nodes.map((one) => {
|
|
44
|
+
const part = types.slice(next, next + one.length);
|
|
45
|
+
next += one.length;
|
|
46
|
+
return part.filter((type) => type !== undefined);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
18
49
|
/** The global array types, whose element type is what an array value carries. */
|
|
19
50
|
function arrayTargets(project) {
|
|
20
51
|
const queries = project.queries;
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "5.
|
|
5
|
+
corpusVersion: "5.1.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:befd5e68f8d68e859b2a0920464a5470a157d5d40a881d9cca28716a4575751d",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/erased-stores.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The values the program stores in a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
6
|
-
* identified by the checker's type identity, so two containers of one type share what
|
|
2
|
+
* The values the program stores in a property, an index signature, an array element or a
|
|
3
|
+
* variable. A serializer emits the value a slot holds, so it walks each stored value at its
|
|
4
|
+
* own static type and each type it is checked against, whatever supertype the slot names.
|
|
5
|
+
* Containers are identified by the checker's type identity, so two of one type share what
|
|
7
6
|
* 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
|
|
7
|
+
* declare hands the value to the code that declares it.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import {
|
|
@@ -31,6 +30,7 @@ import {
|
|
|
31
30
|
import { SymbolFlags, TypeFlags, type Type } from "@typescript/native/sync";
|
|
32
31
|
import { must } from "./query.ts";
|
|
33
32
|
import type { ProjectView } from "./session.ts";
|
|
33
|
+
import { checkedTypes } from "./value-reach.ts";
|
|
34
34
|
|
|
35
35
|
/** The types that keep nothing of the value they are given. */
|
|
36
36
|
const ERASED = TypeFlags.Any | TypeFlags.Unknown;
|
|
@@ -348,15 +348,27 @@ export function erasedStores<Brand>(project: ProjectView<Brand>): ErasedStores {
|
|
|
348
348
|
(at, step) => (at === undefined ? undefined : slotOf(at, String(step))),
|
|
349
349
|
type,
|
|
350
350
|
);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
/** The types of what each stored value puts in its slot: its own and those it is checked against. */
|
|
352
|
+
const storedTypes = (stored: readonly Stored[]): readonly (readonly Type[])[] => {
|
|
353
|
+
const checked = checkedTypes(
|
|
354
|
+
project,
|
|
355
|
+
stored.map((one) => one.value),
|
|
356
|
+
);
|
|
357
|
+
return queries
|
|
358
|
+
.typesAt(stored.map((one) => one.value))
|
|
359
|
+
.map((type, at) =>
|
|
360
|
+
[must(type), ...(checked[at] ?? [])].flatMap(
|
|
361
|
+
(one) => partOf(one, stored[at]?.path ?? []) ?? [],
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
const valueTypes = storedTypes(stores);
|
|
354
366
|
const inside = new Map<number, Type[]>();
|
|
355
367
|
const outside: OutsideStore[] = [];
|
|
356
368
|
stores.forEach((store, at) => {
|
|
357
369
|
const container = containerOf.get(store);
|
|
358
|
-
const
|
|
359
|
-
if (container === undefined
|
|
370
|
+
const values = valueTypes[at] ?? [];
|
|
371
|
+
if (container === undefined) {
|
|
360
372
|
return;
|
|
361
373
|
}
|
|
362
374
|
const parts =
|
|
@@ -370,9 +382,9 @@ export function erasedStores<Brand>(project: ProjectView<Brand>): ErasedStores {
|
|
|
370
382
|
const { name } = store;
|
|
371
383
|
const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
|
|
372
384
|
if (erased && declaredOutside(part)) {
|
|
373
|
-
outside.push({ value: store.value, type
|
|
385
|
+
outside.push(...values.map((type) => ({ value: store.value, type, property: name })));
|
|
374
386
|
} else {
|
|
375
|
-
inside.set(part.id, [...(inside.get(part.id) ?? []),
|
|
387
|
+
inside.set(part.id, [...(inside.get(part.id) ?? []), ...values]);
|
|
376
388
|
}
|
|
377
389
|
}
|
|
378
390
|
});
|
|
@@ -386,15 +398,13 @@ export function erasedStores<Brand>(project: ProjectView<Brand>): ErasedStores {
|
|
|
386
398
|
const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
|
|
387
399
|
return handle === undefined ? undefined : project.declarationAt(handle)?.node;
|
|
388
400
|
};
|
|
389
|
-
const assignedTypes =
|
|
390
|
-
.typesAt(assigned.map((one) => one.value))
|
|
391
|
-
.map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
|
|
401
|
+
const assignedTypes = storedTypes(assigned);
|
|
392
402
|
const byVariable = new Map<Node, Type[]>();
|
|
393
403
|
assigned.forEach((_one, at) => {
|
|
394
404
|
const declaration = declarationOf(symbols[at]);
|
|
395
|
-
const
|
|
396
|
-
if (declaration !== undefined && slots.has(declaration) &&
|
|
397
|
-
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []),
|
|
405
|
+
const types = assignedTypes[at] ?? [];
|
|
406
|
+
if (declaration !== undefined && slots.has(declaration) && types.length > 0) {
|
|
407
|
+
byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), ...types]);
|
|
398
408
|
}
|
|
399
409
|
});
|
|
400
410
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
isArrayLiteralExpression,
|
|
12
12
|
isArrowFunction,
|
|
13
|
+
isAsExpression,
|
|
13
14
|
isCallExpression,
|
|
14
15
|
isClassDeclaration,
|
|
15
16
|
isClassExpression,
|
|
@@ -18,17 +19,22 @@ import {
|
|
|
18
19
|
isFunctionExpression,
|
|
19
20
|
isGetAccessorDeclaration,
|
|
20
21
|
isIdentifier,
|
|
22
|
+
isInterfaceDeclaration,
|
|
21
23
|
isMethodDeclaration,
|
|
22
24
|
isNoSubstitutionTemplateLiteral,
|
|
23
25
|
isNumericLiteral,
|
|
24
26
|
isObjectLiteralExpression,
|
|
25
27
|
isParenthesizedExpression,
|
|
26
28
|
isPropertyAccessExpression,
|
|
29
|
+
isPropertyDeclaration,
|
|
30
|
+
isSatisfiesExpression,
|
|
27
31
|
isSetAccessorDeclaration,
|
|
28
32
|
isSpreadElement,
|
|
29
33
|
isStringLiteral,
|
|
34
|
+
isTypeLiteralNode,
|
|
30
35
|
isVariableDeclaration,
|
|
31
36
|
ModifierFlags,
|
|
37
|
+
NodeFlags,
|
|
32
38
|
SyntaxKind,
|
|
33
39
|
type CallExpression,
|
|
34
40
|
type Expression,
|
|
@@ -58,7 +64,7 @@ import { renderPosition } from "./position.ts";
|
|
|
58
64
|
import { must } from "./query.ts";
|
|
59
65
|
import type { ProjectView } from "./session.ts";
|
|
60
66
|
import { erasedStores, type ErasedStores } from "./erased-stores.ts";
|
|
61
|
-
import { valueReach, type Reached } from "./value-reach.ts";
|
|
67
|
+
import { checkedTypes, valueReach, type Reached } from "./value-reach.ts";
|
|
62
68
|
|
|
63
69
|
/** The serializer every run reads, whatever the configuration names. */
|
|
64
70
|
const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
|
|
@@ -359,6 +365,73 @@ function handedOn<Brand>(
|
|
|
359
365
|
});
|
|
360
366
|
}
|
|
361
367
|
|
|
368
|
+
/**
|
|
369
|
+
* The symbols of the interfaces and object types code of the analysis implements: those a
|
|
370
|
+
* class's `implements` and `extends` clauses name, those an object literal is typed as by
|
|
371
|
+
* a variable or property annotation or a `satisfies` or `as` expression, and every type
|
|
372
|
+
* each of those extends. Read at the first call, which asks the checker.
|
|
373
|
+
*/
|
|
374
|
+
function implementedOwners<Brand>(
|
|
375
|
+
project: ProjectView<Brand>,
|
|
376
|
+
inAnalysis: (path: string) => boolean,
|
|
377
|
+
): () => ReadonlySet<number> {
|
|
378
|
+
let owners: Set<number> | undefined;
|
|
379
|
+
return () => {
|
|
380
|
+
if (owners !== undefined) {
|
|
381
|
+
return owners;
|
|
382
|
+
}
|
|
383
|
+
const found = new Set<number>();
|
|
384
|
+
const queries = project.queries;
|
|
385
|
+
const own = project.ownPaths();
|
|
386
|
+
const files = [
|
|
387
|
+
...project.ownSourceFiles(),
|
|
388
|
+
...project.program
|
|
389
|
+
.getSourceFileNames()
|
|
390
|
+
.filter((name) => !own.has(name) && inAnalysis(name))
|
|
391
|
+
.flatMap((name) => project.program.getSourceFile(name) ?? []),
|
|
392
|
+
];
|
|
393
|
+
const naming: Node[] = [];
|
|
394
|
+
const visit = (node: Node): void => {
|
|
395
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
396
|
+
naming.push(...(node.heritageClauses ?? []).flatMap((clause) => [...clause.types]));
|
|
397
|
+
} else if (
|
|
398
|
+
(isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
|
|
399
|
+
node.type !== undefined &&
|
|
400
|
+
node.initializer !== undefined &&
|
|
401
|
+
isObjectLiteralExpression(evaluated(node.initializer))
|
|
402
|
+
) {
|
|
403
|
+
naming.push(node.type);
|
|
404
|
+
} else if (
|
|
405
|
+
(isSatisfiesExpression(node) || isAsExpression(node)) &&
|
|
406
|
+
isObjectLiteralExpression(evaluated(node.expression))
|
|
407
|
+
) {
|
|
408
|
+
naming.push(node.type);
|
|
409
|
+
}
|
|
410
|
+
node.forEachChild(visit);
|
|
411
|
+
};
|
|
412
|
+
for (const file of files) {
|
|
413
|
+
file.forEachChild(visit);
|
|
414
|
+
}
|
|
415
|
+
const pending = typesAt(project, naming).filter((type) => type !== undefined);
|
|
416
|
+
const seen = new Set<number>();
|
|
417
|
+
for (let type = pending.pop(); type !== undefined; type = pending.pop()) {
|
|
418
|
+
for (const part of must(queries.constituents(type))) {
|
|
419
|
+
const target = must(queries.targetOf(part));
|
|
420
|
+
if (!seen.has(target.id)) {
|
|
421
|
+
seen.add(target.id);
|
|
422
|
+
const symbol = must(queries.symbolOfType(target));
|
|
423
|
+
if (symbol !== undefined) {
|
|
424
|
+
found.add(symbol.id);
|
|
425
|
+
}
|
|
426
|
+
pending.push(...must(queries.baseTypes(target)));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
owners = found;
|
|
431
|
+
return owners;
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
362
435
|
/** The destination of each call's arguments, with the project's forwarding functions settled. */
|
|
363
436
|
interface Destinations {
|
|
364
437
|
/** The function one call invokes and the arguments its parameters bind. */
|
|
@@ -372,10 +445,10 @@ interface Destinations {
|
|
|
372
445
|
/**
|
|
373
446
|
* The destinations of one project's calls. A call is one at every argument where its
|
|
374
447
|
* callee is a serializer an entry names; at the arguments its resolved signature types
|
|
375
|
-
* `unknown` or `any` where its callee is declared outside the analysis
|
|
376
|
-
* callee is a function of the project, at each
|
|
377
|
-
* destination, holding back the widest of what those
|
|
378
|
-
* settled by widening until no parameter moves.
|
|
448
|
+
* `unknown` or `any` where its callee is declared outside the analysis or only ambiently
|
|
449
|
+
* with no implementation in it; and, where its callee is a function of the project, at each
|
|
450
|
+
* parameter the function hands on to a destination, holding back the widest of what those
|
|
451
|
+
* destinations hold back, which is settled by widening until no parameter moves.
|
|
379
452
|
*/
|
|
380
453
|
function destinations<Brand>(
|
|
381
454
|
input: DetectorInput<Brand>,
|
|
@@ -407,13 +480,31 @@ function destinations<Brand>(
|
|
|
407
480
|
for (const parameter of erased.values()) {
|
|
408
481
|
byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
|
|
409
482
|
}
|
|
410
|
-
const
|
|
483
|
+
const inAnalysis = (path: string): boolean =>
|
|
484
|
+
own.has(path) || input.consumers.some((root) => path === root || path.startsWith(`${root}/`));
|
|
485
|
+
const implemented = implementedOwners(project, inAnalysis);
|
|
486
|
+
/**
|
|
487
|
+
* Whether every declaration of a callee is outside the analysis or ambient. An ambient
|
|
488
|
+
* declaration has no body, so the code that runs is outside the analysis, unless it is a
|
|
489
|
+
* member of an interface or an object type a class or a typed object literal of the
|
|
490
|
+
* analysis implements; `settled` false skips that question, which asks the checker.
|
|
491
|
+
*/
|
|
492
|
+
const outside = (target: TSSymbol, settled: boolean): boolean =>
|
|
411
493
|
target.declarations.length > 0 &&
|
|
412
|
-
target.declarations.every(
|
|
413
|
-
(handle)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
494
|
+
target.declarations.every((handle) => {
|
|
495
|
+
if (!inAnalysis(handle.path)) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
const node = project.declarationAt(handle)?.node;
|
|
499
|
+
if (node === undefined || (node.flags & NodeFlags.Ambient) === 0) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
if (!settled || !(isInterfaceDeclaration(node.parent) || isTypeLiteralNode(node.parent))) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
const owner = must(queries.parentOf(target));
|
|
506
|
+
return owner === undefined || !implemented().has(owner.id);
|
|
507
|
+
});
|
|
417
508
|
const invocation = (call: CallExpression): Invocation => {
|
|
418
509
|
const form = forms.get(call);
|
|
419
510
|
const method = form === undefined ? undefined : callees.get(form.method);
|
|
@@ -422,7 +513,7 @@ function destinations<Brand>(
|
|
|
422
513
|
if (
|
|
423
514
|
form !== undefined &&
|
|
424
515
|
method !== undefined &&
|
|
425
|
-
outside(method) &&
|
|
516
|
+
outside(method, false) &&
|
|
426
517
|
FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
|
|
427
518
|
) {
|
|
428
519
|
return form.invocation;
|
|
@@ -472,16 +563,15 @@ function destinations<Brand>(
|
|
|
472
563
|
if (entry !== undefined) {
|
|
473
564
|
return { reach: "properties", callee: spelledEntry(entry.entry, held) };
|
|
474
565
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return outside(target) && erasedAt(call, argument)
|
|
566
|
+
if (outside(target, true)) {
|
|
567
|
+
return erasedAt(call, argument)
|
|
478
568
|
? {
|
|
479
569
|
reach: "conversions",
|
|
480
570
|
callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
|
|
481
571
|
}
|
|
482
572
|
: undefined;
|
|
483
573
|
}
|
|
484
|
-
for (const fn of
|
|
574
|
+
for (const fn of chains.declarationsOf(target)) {
|
|
485
575
|
const parameter = (byFunction.get(fn) ?? []).find(
|
|
486
576
|
(one) => one.index === argument || (one.rest && one.index <= argument),
|
|
487
577
|
);
|
|
@@ -512,16 +602,12 @@ function destinations<Brand>(
|
|
|
512
602
|
if (target === undefined) {
|
|
513
603
|
return false;
|
|
514
604
|
}
|
|
515
|
-
|
|
516
|
-
if (named.some((one) => names(one, target, chains))) {
|
|
605
|
+
if (named.some((one) => names(one, target, chains)) || outside(target, false)) {
|
|
517
606
|
return true;
|
|
518
607
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return declared.some((fn) =>
|
|
523
|
-
(byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))),
|
|
524
|
-
);
|
|
608
|
+
return chains
|
|
609
|
+
.declarationsOf(target)
|
|
610
|
+
.some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
|
|
525
611
|
};
|
|
526
612
|
return { invocation, at, mayReceive };
|
|
527
613
|
}
|
|
@@ -567,7 +653,11 @@ function flowsOf<Brand>(
|
|
|
567
653
|
const typeOf = new Map<Expression, Type | undefined>(
|
|
568
654
|
asked.map((one, index) => [one.value, types[index]]),
|
|
569
655
|
);
|
|
570
|
-
const
|
|
656
|
+
const checked = checkedTypes(
|
|
657
|
+
project,
|
|
658
|
+
arguments_.map((one) => one.value),
|
|
659
|
+
);
|
|
660
|
+
const passed = arguments_.flatMap((one, index) => {
|
|
571
661
|
const type = typeOf.get(one.value);
|
|
572
662
|
let reached: Reached = { classes: new Set<string>(), members: [] };
|
|
573
663
|
if (one.value.kind === SyntaxKind.ThisKeyword) {
|
|
@@ -578,7 +668,7 @@ function flowsOf<Brand>(
|
|
|
578
668
|
} else if (type !== undefined) {
|
|
579
669
|
reached = reach.reachedFrom(type);
|
|
580
670
|
}
|
|
581
|
-
for (const stored of stores.ofVariable(one.value) ?? []) {
|
|
671
|
+
for (const stored of [...(checked[index] ?? []), ...(stores.ofVariable(one.value) ?? [])]) {
|
|
582
672
|
reached = joined(reached, reach.reachedFrom(stored));
|
|
583
673
|
}
|
|
584
674
|
const carries = reached.classes.size > 0 || reached.members.length > 0;
|
package/src/value-reach.ts
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* interface or class of another program, so an instance held only there is missed.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
isAsExpression,
|
|
12
|
+
isParenthesizedExpression,
|
|
13
|
+
isSatisfiesExpression,
|
|
14
|
+
isTypeAssertion,
|
|
15
|
+
type Node,
|
|
16
|
+
} from "@typescript/native/ast";
|
|
10
17
|
import {
|
|
11
18
|
ObjectFlags,
|
|
12
19
|
SymbolFlags,
|
|
@@ -44,6 +51,42 @@ interface ValueReach {
|
|
|
44
51
|
/** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
|
|
45
52
|
const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
|
|
46
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The types each value is checked against by the `satisfies` expressions and type assertions
|
|
56
|
+
* it is written as, through parentheses, and the innermost operand's own type where there is
|
|
57
|
+
* one. A `satisfies` expression's static type is its operand's, so an object literal it
|
|
58
|
+
* checks carries its own members and not the target's, though the literal's properties
|
|
59
|
+
* write the target's; an assertion to `unknown` or `any` hides the operand's type.
|
|
60
|
+
*/
|
|
61
|
+
export function checkedTypes<Brand>(
|
|
62
|
+
project: ProjectView<Brand>,
|
|
63
|
+
values: readonly Node[],
|
|
64
|
+
): readonly (readonly Type[])[] {
|
|
65
|
+
const nodes = values.map((value) => {
|
|
66
|
+
const found: Node[] = [];
|
|
67
|
+
let at = value;
|
|
68
|
+
while (
|
|
69
|
+
isParenthesizedExpression(at) ||
|
|
70
|
+
isSatisfiesExpression(at) ||
|
|
71
|
+
isAsExpression(at) ||
|
|
72
|
+
isTypeAssertion(at)
|
|
73
|
+
) {
|
|
74
|
+
if (!isParenthesizedExpression(at)) {
|
|
75
|
+
found.push(at.type);
|
|
76
|
+
}
|
|
77
|
+
at = at.expression;
|
|
78
|
+
}
|
|
79
|
+
return found.length === 0 ? found : [...found, at];
|
|
80
|
+
});
|
|
81
|
+
const types = typesAt(project, nodes.flat());
|
|
82
|
+
let next = 0;
|
|
83
|
+
return nodes.map((one) => {
|
|
84
|
+
const part = types.slice(next, next + one.length);
|
|
85
|
+
next += one.length;
|
|
86
|
+
return part.filter((type): type is Type => type !== undefined);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
/** The global array types, whose element type is what an array value carries. */
|
|
48
91
|
function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
|
|
49
92
|
const queries = project.queries;
|
package/src/version.ts
CHANGED