@cplieger/deadset-ts 8.0.0-dev.3 → 8.0.0-dev.5

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.
@@ -11,10 +11,13 @@ import {
11
11
  isArrayLiteralExpression,
12
12
  isArrowFunction,
13
13
  isAsExpression,
14
+ isAwaitExpression,
14
15
  isCallExpression,
15
16
  isClassDeclaration,
16
17
  isClassExpression,
17
18
  isClassStaticBlockDeclaration,
19
+ isElementAccessExpression,
20
+ isExpressionStatement,
18
21
  isFunctionDeclaration,
19
22
  isFunctionExpression,
20
23
  isFunctionLikeDeclaration,
@@ -22,6 +25,7 @@ import {
22
25
  isIdentifier,
23
26
  isInterfaceDeclaration,
24
27
  isMethodDeclaration,
28
+ isNonNullExpression,
25
29
  isNoSubstitutionTemplateLiteral,
26
30
  isNumericLiteral,
27
31
  isObjectLiteralExpression,
@@ -29,10 +33,14 @@ import {
29
33
  isForInStatement,
30
34
  isPropertyAccessExpression,
31
35
  isPropertyDeclaration,
36
+ isReturnStatement,
32
37
  isSatisfiesExpression,
33
38
  isSetAccessorDeclaration,
39
+ isSourceFile,
40
+ isSpreadAssignment,
34
41
  isSpreadElement,
35
42
  isStringLiteral,
43
+ isTypeAssertion,
36
44
  isTypeLiteralNode,
37
45
  isVariableDeclaration,
38
46
  ModifierFlags,
@@ -42,6 +50,8 @@ import {
42
50
  type Expression,
43
51
  type Node,
44
52
  type ParameterDeclaration,
53
+ type SourceFile,
54
+ type SpreadAssignment,
45
55
  } from "@typescript/native/ast";
46
56
  import {
47
57
  SignatureKind,
@@ -64,8 +74,8 @@ import {
64
74
  } from "./configured-declarations.ts";
65
75
  import type { DetectorInput, Evidence } from "./exempt.ts";
66
76
  import { nodeKey, type Inventory } from "./inventory.ts";
67
- import { renderPosition } from "./position.ts";
68
- import { must } from "./query.ts";
77
+ import { renderPosition, type Position } from "./position.ts";
78
+ import { must, Unanswerable } from "./query.ts";
69
79
  import type { ProjectView } from "./session.ts";
70
80
  import { erasedStores, type ErasedStores } from "./erased-stores.ts";
71
81
  import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
@@ -82,6 +92,9 @@ const SERIALIZED_CONVERSION = "toJSON";
82
92
  /** The parameter types that keep nothing of the value they are given. */
83
93
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
84
94
 
95
+ /** The standard library's promise types, whose value is their one type argument. */
96
+ const PROMISES: ReadonlySet<string> = new Set(["Promise", "PromiseLike"]);
97
+
85
98
  /** How deep a callee's result is searched for a type parameter: `Promise<Box<T>>` is two levels. */
86
99
  const MENTION_DEPTH = 3;
87
100
 
@@ -261,6 +274,8 @@ function throughMethod(call: CallExpression): MethodForm | undefined {
261
274
  interface Walked {
262
275
  readonly sites: readonly Site[];
263
276
  readonly parameters: readonly Parameter[];
277
+ /** The class properties whose declaration writes an initializer, by identifier. */
278
+ readonly initialized: ReadonlySet<string>;
264
279
  }
265
280
 
266
281
  /**
@@ -271,9 +286,16 @@ interface Walked {
271
286
  function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walked {
272
287
  const sites: Site[] = [];
273
288
  const parameters: Parameter[] = [];
289
+ const initialized = new Set<string>();
274
290
  for (const file of project.ownSourceFiles()) {
275
291
  const visit = (node: Node, self: string | undefined): void => {
276
292
  let inner = self;
293
+ if (isPropertyDeclaration(node) && node.initializer !== undefined) {
294
+ const id = held.declarations.get(nodeKey(file, node));
295
+ if (id !== undefined) {
296
+ initialized.add(id);
297
+ }
298
+ }
277
299
  if (isClassDeclaration(node) || isClassExpression(node)) {
278
300
  inner = held.declarations.get(nodeKey(file, node));
279
301
  } else if (resetsThis(node)) {
@@ -306,7 +328,7 @@ function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walke
306
328
  visit(child, undefined);
307
329
  });
308
330
  }
309
- return { sites, parameters };
331
+ return { sites, parameters, initialized };
310
332
  }
311
333
 
312
334
  /** The calls of `Object` that read every own property of their argument by name. */
@@ -319,17 +341,15 @@ const ENUMERATIONS: ReadonlySet<string> = new Set([
319
341
  ]);
320
342
 
321
343
  /**
322
- * Whether a parameter type keeps no member name of the value it is given: `unknown`, `any`,
323
- * or an object type of a string index signature and no property.
344
+ * Whether a parameter type keeps no member name of the value it is given beyond the
345
+ * properties it names: `unknown`, `any`, or an object type of a string index signature,
346
+ * under which every other member of the value is held.
324
347
  */
325
348
  function keepsNoName<Brand>(project: ProjectView<Brand>, type: Type): boolean {
326
349
  if ((type.flags & ERASED) !== 0) {
327
350
  return true;
328
351
  }
329
- if (
330
- (type.flags & TypeFlags.Object) === 0 ||
331
- must(project.queries.propertiesOf(type)).length > 0
332
- ) {
352
+ if ((type.flags & TypeFlags.Object) === 0) {
333
353
  return false;
334
354
  }
335
355
  return must(project.queries.indexInfosOf(type)).some(
@@ -972,6 +992,39 @@ function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth:
972
992
  ].flatMap((one) => typeParametersOf(project, one, depth + 1));
973
993
  }
974
994
 
995
+ /**
996
+ * The type parameters one type names, by identifier: through constituents, type arguments,
997
+ * an alias's arguments and the parameters and results of its call signatures, so a callback
998
+ * parameter's result names the parameter its callee's result does.
999
+ */
1000
+ function namedTypeParameters<Brand>(
1001
+ project: ProjectView<Brand>,
1002
+ type: Type,
1003
+ depth: number,
1004
+ ): readonly number[] {
1005
+ const { queries } = project;
1006
+ const aliased = (type as { readonly aliasTypeArguments?: readonly number[] }).aliasTypeArguments;
1007
+ if (type.isTypeParameter()) {
1008
+ return [type.id];
1009
+ }
1010
+ if (depth >= MENTION_DEPTH) {
1011
+ return aliased ?? [];
1012
+ }
1013
+ return [
1014
+ ...(aliased ?? []),
1015
+ ...[
1016
+ ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
1017
+ ...must(queries.typeArguments(type)),
1018
+ ...must(queries.signaturesOf(type, SignatureKind.Call)).flatMap((signature) => [
1019
+ ...signature.parameters.flatMap((_parameter, at) => [
1020
+ must(queries.typeAtPosition(signature, at)),
1021
+ ]),
1022
+ ...[must(queries.returnTypeOf(signature))].filter((one) => one !== undefined),
1023
+ ]),
1024
+ ].flatMap((one) => namedTypeParameters(project, one, depth + 1)),
1025
+ ];
1026
+ }
1027
+
975
1028
  /**
976
1029
  * The types an instantiated type puts where a declared type names `parameter`: matched
977
1030
  * through type arguments, and in a union every constituent the declared union does not
@@ -1128,6 +1181,11 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
1128
1181
  (flags & SymbolFlags.Property) !== 0 ||
1129
1182
  ((flags & CONVERSION_MEMBERS) !== 0 &&
1130
1183
  (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
1184
+ // The receiver reads only what the program wrote, so a data member holds while written.
1185
+ const evidence = (id: string, flags: SymbolFlags, detail: string, site: Position): Evidence =>
1186
+ (flags & SymbolFlags.Property) !== 0 && !walked.initialized.has(id)
1187
+ ? { id, detail, site, whileWritten: true }
1188
+ : { id, detail, site };
1131
1189
  const found: Evidence[] = [];
1132
1190
  for (const flow of flows) {
1133
1191
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
@@ -1136,16 +1194,402 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
1136
1194
  const symbol = byId.get(member.id);
1137
1195
  const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
1138
1196
  if (holdsBack(member.flags, name, flow.to)) {
1139
- found.push({ id: member.id, detail, site });
1197
+ found.push(evidence(member.id, member.flags, detail, site));
1140
1198
  }
1141
1199
  }
1142
1200
  for (const member of flow.members) {
1143
1201
  if (holdsBack(member.flags, member.name, flow.to)) {
1144
1202
  for (const id of chains.declarationsOf(member)) {
1145
- found.push({ id, detail, site });
1203
+ found.push(evidence(id, member.flags, detail, site));
1146
1204
  }
1147
1205
  }
1148
1206
  }
1149
1207
  }
1150
1208
  return found;
1151
1209
  }
1210
+
1211
+ /** The data members the decodes and copies of a project's production code fill. */
1212
+ export interface Decoded {
1213
+ /**
1214
+ * Those a generic declared outside the analysis fills, which it may read, and those of
1215
+ * a value another call or a property types `any` or `unknown`, whose maker it cannot see.
1216
+ */
1217
+ readonly filled: readonly string[];
1218
+ /** Those a `JSON.parse` call of the standard library fills. */
1219
+ readonly parsed: readonly string[];
1220
+ /** Those a spread or `Object.assign` copies from a value whose type has a member of the name. */
1221
+ readonly copied: readonly string[];
1222
+ /** Every data member of the project, where the checker left a question unanswered. */
1223
+ readonly unanswered: readonly string[];
1224
+ }
1225
+
1226
+ /**
1227
+ * The data members the decodes and copies of `files` fill, as {@link Decoded} names them.
1228
+ * A generic declared outside the analysis, or only ambiently, fills the type that a type
1229
+ * parameter of its own no parameter names binds, where the awaited result carries that
1230
+ * type through its data members rather than only naming it, as a store's methods do: no
1231
+ * argument hands the value over, so it fills it where the analysis cannot see, and may
1232
+ * read it. Where the checker leaves a question unanswered, every data member is `unanswered`.
1233
+ */
1234
+ export function decodedMembers<Brand>(
1235
+ project: ProjectView<Brand>,
1236
+ held: Inventory,
1237
+ files: readonly SourceFile[],
1238
+ consumers: readonly string[],
1239
+ ): Decoded {
1240
+ try {
1241
+ return { ...decodedOf(project, held, files, consumers), unanswered: [] };
1242
+ } catch (error: unknown) {
1243
+ if (!(error instanceof Unanswerable)) {
1244
+ throw error;
1245
+ }
1246
+ return {
1247
+ filled: [],
1248
+ parsed: [],
1249
+ copied: [],
1250
+ unanswered: held.symbols
1251
+ .filter((symbol) => symbol.kind === "class-member" || symbol.kind === "type-member")
1252
+ .map((symbol) => symbol.id),
1253
+ };
1254
+ }
1255
+ }
1256
+
1257
+ function decodedOf<Brand>(
1258
+ project: ProjectView<Brand>,
1259
+ held: Inventory,
1260
+ files: readonly SourceFile[],
1261
+ consumers: readonly string[],
1262
+ ): Omit<Decoded, "unanswered"> {
1263
+ const { queries } = project;
1264
+ const own = project.ownPaths();
1265
+ const inAnalysis = (path: string): boolean =>
1266
+ own.has(path) || consumers.some((root) => path === root || path.startsWith(`${root}/`));
1267
+ const calls: CallExpression[] = [];
1268
+ const untyped: Expression[] = [];
1269
+ const spreads: SpreadAssignment[] = [];
1270
+ const noteRead = (node: Expression): void => {
1271
+ const at = readAs(node);
1272
+ if (at.tops.length > 0 || at.asserted.length > 0) {
1273
+ untyped.push(node);
1274
+ }
1275
+ };
1276
+ const visit = (node: Node): void => {
1277
+ if (isCallExpression(node)) {
1278
+ calls.push(node);
1279
+ noteRead(node);
1280
+ } else if (isPropertyAccessExpression(node) || isElementAccessExpression(node)) {
1281
+ noteRead(node);
1282
+ } else if (isSpreadAssignment(node)) {
1283
+ spreads.push(node);
1284
+ }
1285
+ node.forEachChild(visit);
1286
+ };
1287
+ for (const file of files) {
1288
+ file.forEachChild(visit);
1289
+ }
1290
+ const filled: { readonly carried: Type; readonly types: readonly Type[] }[] = [];
1291
+ const resolved = queries.resolvedSignatures(calls).map(must);
1292
+ for (const signature of resolved) {
1293
+ const declared =
1294
+ signature?.target === undefined ? undefined : must(queries.genericOf(signature));
1295
+ const declaration = declared?.declaration;
1296
+ const generic = declared?.typeParameters ?? [];
1297
+ if (signature === undefined || declared === undefined || generic.length === 0) {
1298
+ continue;
1299
+ }
1300
+ const node = declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
1301
+ if (
1302
+ declaration === undefined ||
1303
+ (inAnalysis(declaration.path) &&
1304
+ (node === undefined || (node.flags & NodeFlags.Ambient) === 0))
1305
+ ) {
1306
+ continue;
1307
+ }
1308
+ const result = must(queries.returnTypeOf(declared));
1309
+ const given = must(queries.returnTypeOf(signature));
1310
+ if (result === undefined || given === undefined) {
1311
+ continue;
1312
+ }
1313
+ const passed = new Set(
1314
+ declared.parameters.flatMap((_parameter, at) => {
1315
+ const type = must(queries.parameterType(declared, at));
1316
+ return type === undefined ? [] : namedTypeParameters(project, type, 0);
1317
+ }),
1318
+ );
1319
+ const types = typeParametersOf(project, result, 0)
1320
+ .filter((parameter) => generic.includes(parameter.id) && !passed.has(parameter.id))
1321
+ .flatMap((parameter) => bound(project, result, given, parameter, 0));
1322
+ if (types.length > 0) {
1323
+ filled.push({ carried: awaited(project, given), types });
1324
+ }
1325
+ }
1326
+ const read = readTypes(project, untyped);
1327
+ const copies = copiedNames(project, spreads, calls);
1328
+ if (
1329
+ filled.length === 0 &&
1330
+ read.parsed.length === 0 &&
1331
+ read.made.length === 0 &&
1332
+ copies.length === 0
1333
+ ) {
1334
+ return { filled: [], parsed: [], copied: [] };
1335
+ }
1336
+ const reach = valueReach(project, held);
1337
+ const chains = aliasChains(project, held);
1338
+ const members = (type: Type): readonly string[] => {
1339
+ const reached = reach.reachedFrom(type);
1340
+ const ofClasses = classMembers(project, held, reached.classes, "instance");
1341
+ return [
1342
+ ...[...reached.classes].flatMap((id) =>
1343
+ (ofClasses.get(id) ?? []).flatMap((member) =>
1344
+ (member.flags & SymbolFlags.Property) !== 0 ? [member.id] : [],
1345
+ ),
1346
+ ),
1347
+ ...reached.members.flatMap((member) =>
1348
+ (member.flags & SymbolFlags.Property) !== 0 ? chains.declarationsOf(member) : [],
1349
+ ),
1350
+ ];
1351
+ };
1352
+ return {
1353
+ filled: [
1354
+ ...new Set([
1355
+ ...filled.flatMap(({ carried, types }) => {
1356
+ const carries = new Set(members(carried));
1357
+ return types.flatMap((type) => members(type).filter((id) => carries.has(id)));
1358
+ }),
1359
+ ...read.made.flatMap(members),
1360
+ ]),
1361
+ ],
1362
+ parsed: [...new Set(read.parsed.flatMap(members))],
1363
+ copied: [
1364
+ ...new Set(
1365
+ copies.flatMap(({ into, names }) =>
1366
+ must(queries.constituents(into)).flatMap((part) =>
1367
+ must(queries.propertiesOf(part)).flatMap((property) =>
1368
+ (property.flags & SymbolFlags.Property) !== 0 && names.has(property.name)
1369
+ ? chains.declarationsOf(property)
1370
+ : [],
1371
+ ),
1372
+ ),
1373
+ ),
1374
+ ),
1375
+ ],
1376
+ };
1377
+ }
1378
+
1379
+ /** Where a value is read as a type: the assertions around it, and the positions it is written in. */
1380
+ interface ReadAs {
1381
+ /** The type nodes of the `as` and angle-bracket assertions around the value. */
1382
+ readonly asserted: readonly Node[];
1383
+ /** The outermost expressions whose contextual types the value is read as. */
1384
+ readonly tops: readonly Expression[];
1385
+ }
1386
+
1387
+ /**
1388
+ * Where one value is read as a type: each `as` and angle-bracket assertion around it,
1389
+ * through parentheses, `await` and `!`, and the outermost expression unless a statement
1390
+ * of its own or the object or callee of a further access holds it. A `satisfies` check
1391
+ * is stepped through and types nothing, its expression's type being its operand's. A
1392
+ * callback's result is read as well where the call taking the callback is, which may
1393
+ * hand it on inside its result, as `map` does.
1394
+ */
1395
+ function readAs(value: Expression, depth = 0): ReadAs {
1396
+ const asserted: Node[] = [];
1397
+ let at: Expression = value;
1398
+ for (;;) {
1399
+ const parent = at.parent;
1400
+ if (isAsExpression(parent) || isTypeAssertion(parent)) {
1401
+ asserted.push(parent.type);
1402
+ } else if (
1403
+ !isParenthesizedExpression(parent) &&
1404
+ !isAwaitExpression(parent) &&
1405
+ !isNonNullExpression(parent) &&
1406
+ !isSatisfiesExpression(parent)
1407
+ ) {
1408
+ break;
1409
+ }
1410
+ at = parent;
1411
+ }
1412
+ const parent = at.parent;
1413
+ const unused =
1414
+ isExpressionStatement(parent) ||
1415
+ ((isPropertyAccessExpression(parent) ||
1416
+ isElementAccessExpression(parent) ||
1417
+ isCallExpression(parent)) &&
1418
+ parent.expression === at);
1419
+ const call = depth < MENTION_DEPTH ? callTaking(at) : undefined;
1420
+ const outer = call === undefined ? undefined : readAs(call, depth + 1);
1421
+ return {
1422
+ asserted: [...asserted, ...(outer?.asserted ?? [])],
1423
+ tops: [...(unused ? [] : [at]), ...(outer?.tops ?? [])],
1424
+ };
1425
+ }
1426
+
1427
+ /** The call one value is returned to as a callback's result, an argument of that call. */
1428
+ function callTaking(value: Expression): CallExpression | undefined {
1429
+ const parent = value.parent;
1430
+ let callback: Node | undefined;
1431
+ if (isArrowFunction(parent) && parent.body === value) {
1432
+ callback = parent;
1433
+ } else if (isReturnStatement(parent)) {
1434
+ let at: Node = parent.parent;
1435
+ while (!isFunctionLikeDeclaration(at) && !isSourceFile(at)) {
1436
+ at = at.parent;
1437
+ }
1438
+ callback = isArrowFunction(at) || isFunctionExpression(at) ? at : undefined;
1439
+ }
1440
+ let at = callback?.parent;
1441
+ while (at !== undefined && isParenthesizedExpression(at)) {
1442
+ at = at.parent;
1443
+ }
1444
+ return at !== undefined && isCallExpression(at) && at.expression !== callback ? at : undefined;
1445
+ }
1446
+
1447
+ /** The global the standard library declares one name of `JSON` or `Object` as. */
1448
+ function standardGlobal(callee: Expression, global: string, name: string): Node | undefined {
1449
+ return isPropertyAccessExpression(callee) &&
1450
+ isIdentifier(callee.expression) &&
1451
+ callee.expression.text === global &&
1452
+ callee.name.text === name
1453
+ ? callee.expression
1454
+ : undefined;
1455
+ }
1456
+
1457
+ /**
1458
+ * The types each value of `values` typed `any` or `unknown` is read as ({@link readAs}):
1459
+ * `parsed` for a `JSON.parse` call of the standard library, `made` for any other call or
1460
+ * property, whose maker the analysis cannot see. `JSON` must be the standard library's
1461
+ * global.
1462
+ */
1463
+ function readTypes<Brand>(
1464
+ project: ProjectView<Brand>,
1465
+ values: readonly Expression[],
1466
+ ): { readonly parsed: readonly Type[]; readonly made: readonly Type[] } {
1467
+ if (values.length === 0) {
1468
+ return { parsed: [], made: [] };
1469
+ }
1470
+ const own = project.ownPaths();
1471
+ const parses = values.map((value) =>
1472
+ isCallExpression(value) ? standardGlobal(value.expression, "JSON", "parse") : undefined,
1473
+ );
1474
+ const globals = parses.flatMap((global) => (global === undefined ? [] : [global]));
1475
+ const standard = new Set(
1476
+ project
1477
+ .symbolsAt(globals.map((global) => project.handle(global)))
1478
+ .map(must)
1479
+ .flatMap((symbol, at) =>
1480
+ symbol?.declarations.every((handle) => !own.has(handle.path)) === true ? [globals[at]] : [],
1481
+ ),
1482
+ );
1483
+ // An `await` around the value reads the value it resolves to.
1484
+ const valueTypes = typesAt(
1485
+ project,
1486
+ values.map((value) => (isAwaitExpression(value.parent) ? value.parent : value)),
1487
+ );
1488
+ const sites = values.flatMap((value, at) => {
1489
+ const parse = parses[at];
1490
+ const parsed = parse !== undefined && standard.has(parse);
1491
+ const type = valueTypes[at];
1492
+ const untyped = type !== undefined && (type.flags & (TypeFlags.Any | TypeFlags.Unknown)) !== 0;
1493
+ return parsed || untyped ? [{ ...readAs(value), parsed }] : [];
1494
+ });
1495
+ // Each type node and each outermost expression, with whether a `JSON.parse` call is read as it.
1496
+ const typed = sites.flatMap((site) =>
1497
+ site.asserted.map((node) => ({ node, parsed: site.parsed })),
1498
+ );
1499
+ const placed = sites.flatMap((site) => site.tops.map((node) => ({ node, parsed: site.parsed })));
1500
+ const read = [
1501
+ ...typesAt(
1502
+ project,
1503
+ typed.map((one) => one.node),
1504
+ ).map((type, at) => ({ type, parsed: typed[at]?.parsed })),
1505
+ ...project.queries
1506
+ .contextualTypes(placed.map((one) => one.node))
1507
+ .map(must)
1508
+ .map((type, at) => ({ type, parsed: placed[at]?.parsed })),
1509
+ ];
1510
+ const typesWhere = (parsed: boolean): Type[] =>
1511
+ read.flatMap((one) => (one.type !== undefined && one.parsed === parsed ? [one.type] : []));
1512
+ return { parsed: typesWhere(true), made: typesWhere(false) };
1513
+ }
1514
+
1515
+ /**
1516
+ * The copies the spreads and calls of `files` make: for each spread in an object literal,
1517
+ * the type the literal is read as, and for each `Object.assign` call of the standard
1518
+ * library, its first argument's type, with the member names of the copied values' types.
1519
+ */
1520
+ function copiedNames<Brand>(
1521
+ project: ProjectView<Brand>,
1522
+ spreads: readonly SpreadAssignment[],
1523
+ calls: readonly CallExpression[],
1524
+ ): readonly { readonly into: Type; readonly names: ReadonlySet<string> }[] {
1525
+ const own = project.ownPaths();
1526
+ const assigns = calls.flatMap((call) => {
1527
+ const global = standardGlobal(call.expression, "Object", "assign");
1528
+ const [target, ...sources] = call.arguments;
1529
+ return global === undefined || target === undefined || sources.length === 0
1530
+ ? []
1531
+ : [{ global, target, sources: sources.filter((source) => !isSpreadElement(source)) }];
1532
+ });
1533
+ const standard = project
1534
+ .symbolsAt(assigns.map((one) => project.handle(one.global)))
1535
+ .map(must)
1536
+ .map((symbol) => symbol?.declarations.every((handle) => !own.has(handle.path)) === true);
1537
+ const assigned = assigns.filter((_one, at) => standard[at] === true);
1538
+ const literals = spreads.flatMap((spread) =>
1539
+ isObjectLiteralExpression(spread.parent)
1540
+ ? [{ literal: spread.parent, source: spread.expression }]
1541
+ : [],
1542
+ );
1543
+ const contexts = project.queries.contextualTypes(literals.map((one) => one.literal)).map(must);
1544
+ const targets = typesAt(
1545
+ project,
1546
+ assigned.map((one) => one.target),
1547
+ );
1548
+ const spreadTypes = typesAt(
1549
+ project,
1550
+ literals.map((one) => one.source),
1551
+ );
1552
+ const sourceTypes = assigned.map((one) => typesAt(project, one.sources));
1553
+ const namesOf = (types: readonly (Type | undefined)[]): ReadonlySet<string> =>
1554
+ new Set(
1555
+ types.flatMap((type) =>
1556
+ type === undefined
1557
+ ? []
1558
+ : must(project.queries.propertiesOf(type)).map((property) => property.name),
1559
+ ),
1560
+ );
1561
+ const found: { readonly into: Type; readonly names: ReadonlySet<string> }[] = [];
1562
+ const add = (into: Type | undefined, names: ReadonlySet<string>): void => {
1563
+ if (into !== undefined && names.size > 0) {
1564
+ found.push({ into, names });
1565
+ }
1566
+ };
1567
+ contexts.forEach((into, at) => {
1568
+ add(into, namesOf([spreadTypes[at]]));
1569
+ });
1570
+ targets.forEach((into, at) => {
1571
+ add(into, namesOf(sourceTypes[at] ?? []));
1572
+ });
1573
+ return found;
1574
+ }
1575
+
1576
+ /** The value a promise of the standard library resolves to, through nested promises. */
1577
+ function awaited<Brand>(project: ProjectView<Brand>, type: Type): Type {
1578
+ const { queries } = project;
1579
+ let at = type;
1580
+ for (let depth = 0; depth < MENTION_DEPTH; depth += 1) {
1581
+ const symbol = must(queries.symbolOfType(at));
1582
+ const [inner] = must(queries.typeArguments(at));
1583
+ const standard =
1584
+ symbol !== undefined &&
1585
+ PROMISES.has(symbol.name) &&
1586
+ symbol.declarations.every(
1587
+ (handle) => project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true,
1588
+ );
1589
+ if (!standard || inner === undefined) {
1590
+ break;
1591
+ }
1592
+ at = inner;
1593
+ }
1594
+ return at;
1595
+ }
package/src/sweep.ts CHANGED
@@ -33,6 +33,8 @@ export interface Exemption {
33
33
  * The record holds only while one of them is called through its interface.
34
34
  */
35
35
  readonly implementing?: Implementing;
36
+ /** Whether the record holds only where a write the mode counts stores into the declaration. */
37
+ readonly whileWritten?: true;
36
38
  }
37
39
 
38
40
  /** The interface methods one retained class member implements, and whether its body is empty. */
@@ -350,7 +350,6 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
350
350
  try {
351
351
  host.writeDocument(documents.report, json(report));
352
352
  const options = {
353
- failOn: config.reporters.failOn,
354
353
  readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
355
354
  template,
356
355
  };
@@ -97,12 +97,11 @@ export function runCommands(text: string, key: RegExp = RUN_LINE): readonly stri
97
97
  }
98
98
 
99
99
  /**
100
- * Every token the recipe lines of the target root's makefile hold, each with the directory
101
- * below the root it is read against: a recipe line is one that starts with a tab, joined
102
- * with the lines a trailing `\` continues it onto, and make runs each in a shell of its
103
- * own, so a `cd` moves the directory of the later commands of its line alone.
100
+ * The commands of the recipe lines of the target root's makefile, each line's in order: a
101
+ * recipe line is one that starts with a tab, joined with the lines a trailing `\`
102
+ * continues it onto.
104
103
  */
105
- function recipeTokens(host: Host, root: string): readonly { token: string; dir: string }[] {
104
+ function recipeLines(host: Host, root: string): readonly (readonly string[])[] {
106
105
  const path = MAKEFILES.map((name) => joinPath(root, name)).find(
107
106
  (candidate) => host.kindOf(candidate) === "file",
108
107
  );
@@ -110,15 +109,23 @@ function recipeTokens(host: Host, root: string): readonly { token: string; dir:
110
109
  if (text === undefined) {
111
110
  return [];
112
111
  }
112
+ return text
113
+ .replace(/\\\r?\n/gu, " ")
114
+ .split(/\r?\n/u)
115
+ .filter((line) => line.startsWith("\t"))
116
+ .map((line) => withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(COMMAND_SEPARATORS));
117
+ }
118
+
119
+ /**
120
+ * Every token the recipe lines of the target root's makefile hold, each with the directory
121
+ * below the root it is read against: make runs each line in a shell of its own, so a `cd`
122
+ * moves the directory of the later commands of its line alone.
123
+ */
124
+ function recipeTokens(host: Host, root: string): readonly { token: string; dir: string }[] {
113
125
  const found: { token: string; dir: string }[] = [];
114
- for (const line of text.replace(/\\\r?\n/gu, " ").split(/\r?\n/u)) {
115
- if (!line.startsWith("\t")) {
116
- continue;
117
- }
126
+ for (const line of recipeLines(host, root)) {
118
127
  let dir = root;
119
- for (const command of withoutComment(line.slice(1).replace(/^[@+-]+/u, "")).split(
120
- COMMAND_SEPARATORS,
121
- )) {
128
+ for (const command of line) {
122
129
  const tokens = scriptTokens(command);
123
130
  const [first, target, ...rest] = tokens;
124
131
  if (first === "cd" && target !== undefined && rest.length === 0 && !COMPUTED.test(target)) {
@@ -156,13 +163,27 @@ function withoutComment(line: string): string {
156
163
  }
157
164
 
158
165
  /**
159
- * Every token the target root's workflow steps, git hooks and makefile hold: the `run:`
166
+ * Every command the target root's workflow steps, git hooks and makefile run: the `run:`
160
167
  * steps of the workflow files in `.github/workflows`, every line of each file in `.husky`
161
168
  * and `.githooks`, the `entry:` commands of `.pre-commit-config.yaml`, the `run:` commands
162
169
  * of a lefthook file and the makefile's recipe lines.
163
170
  */
171
+ export function workflowCommands(host: Host, targetRoot: string): readonly string[] {
172
+ const root = resolvePath(host.workingDirectory(), targetRoot);
173
+ return [...stepCommands(host, root), ...recipeLines(host, root).flat()];
174
+ }
175
+
176
+ /** Every token the commands {@link workflowCommands} reads hold. */
164
177
  export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
165
178
  const root = resolvePath(host.workingDirectory(), targetRoot);
179
+ return [
180
+ ...stepCommands(host, root).flatMap((command) => scriptTokens(command)),
181
+ ...recipeTokens(host, root).map(({ token }) => token),
182
+ ];
183
+ }
184
+
185
+ /** The commands of the workflow steps, git hooks and hook managers at one root. */
186
+ function stepCommands(host: Host, root: string): readonly string[] {
166
187
  const commands: string[] = [];
167
188
  for (const path of filesIn(host, joinPath(root, WORKFLOWS), (name) => /\.ya?ml$/u.test(name))) {
168
189
  commands.push(...runCommands(readText(host, path) ?? ""));
@@ -175,10 +196,7 @@ export function workflowTokens(host: Host, targetRoot: string): readonly string[
175
196
  for (const [name, key] of HOOK_MANAGERS) {
176
197
  commands.push(...runCommands(readText(host, joinPath(root, name)) ?? "", key));
177
198
  }
178
- return [
179
- ...commands.flatMap((command) => scriptTokens(command)),
180
- ...recipeTokens(host, root).map(({ token }) => token),
181
- ];
199
+ return commands;
182
200
  }
183
201
 
184
202
  /**