@cplieger/deadset-ts 7.0.0 → 7.1.0-dev.1

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.
@@ -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.0.0",
5
+ corpusVersion: "5.1.0",
6
6
  result: "pass",
7
- digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
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 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
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 the type.
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
- const valueTypes = queries
260
- .typesAt(stores.map((one) => one.value))
261
- .map((type, at) => partOf(must(type), stores[at]?.path ?? []));
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 value = valueTypes[at];
267
- if (container === undefined || value === 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: value, property: name });
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) ?? []), value]);
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 = queries
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 type = assignedTypes[at];
301
- if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
302
- byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
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; and, where its
224
- * callee is a function of the project, at each parameter the function hands on to a
225
- * destination, holding back the widest of what those destinations hold back, which is
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 outside = (target) => target.declarations.length > 0 &&
253
- target.declarations.every((handle) => !own.has(handle.path) &&
254
- !input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)));
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
- const declared = chains.declarationsOf(target);
307
- if (declared.length === 0) {
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 declared) {
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
- const declared = chains.declarationsOf(target);
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
- if (declared.length === 0) {
347
- return outside(target);
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 passed = arguments_.flatMap((one) => {
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;
@@ -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;
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "7.0.0";
7
+ export const CONTRACT_VERSION = "7.1.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "7.0.0",
3
+ "version": "7.1.0-dev.1",
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": "7.0.0",
3
+ "version": "7.1.0-dev.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -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.0.0",
5
+ corpusVersion: "5.1.0",
6
6
  result: "pass",
7
- digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
7
+ digest: "sha256:befd5e68f8d68e859b2a0920464a5470a157d5d40a881d9cca28716a4575751d",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -1,11 +1,10 @@
1
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
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 the type.
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
- const valueTypes = queries
352
- .typesAt(stores.map((one) => one.value))
353
- .map((type, at) => partOf(must(type), stores[at]?.path ?? []));
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 value = valueTypes[at];
359
- if (container === undefined || value === 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: value, property: name });
385
+ outside.push(...values.map((type) => ({ value: store.value, type, property: name })));
374
386
  } else {
375
- inside.set(part.id, [...(inside.get(part.id) ?? []), value]);
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 = queries
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 type = assignedTypes[at];
396
- if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
397
- byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
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; and, where its
376
- * callee is a function of the project, at each parameter the function hands on to a
377
- * destination, holding back the widest of what those destinations hold back, which is
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 outside = (target: TSSymbol): boolean =>
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
- !own.has(handle.path) &&
415
- !input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)),
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
- const declared = chains.declarationsOf(target);
476
- if (declared.length === 0) {
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 declared) {
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
- const declared = chains.declarationsOf(target);
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
- if (declared.length === 0) {
520
- return outside(target);
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 passed = arguments_.flatMap((one) => {
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;
@@ -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
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "7.0.0";
7
+ export const CONTRACT_VERSION = "7.1.0";