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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alias-chain.js +2 -2
  3. package/dist/src/analysis.js +89 -44
  4. package/dist/src/augmentations.js +14 -12
  5. package/dist/src/calls.js +2 -2
  6. package/dist/src/class-members.js +1 -1
  7. package/dist/src/components.js +62 -20
  8. package/dist/src/configuration-files.js +6 -6
  9. package/dist/src/configured-declarations.js +2 -2
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/contextual-uses.js +71 -14
  12. package/dist/src/convention-rows.js +6 -2
  13. package/dist/src/conventions.js +4 -2
  14. package/dist/src/dependencies.js +81 -27
  15. package/dist/src/deprecation.js +1 -1
  16. package/dist/src/destructuring.js +2 -2
  17. package/dist/src/discover.js +1 -1
  18. package/dist/src/element-access.js +341 -0
  19. package/dist/src/entry-points.js +2 -2
  20. package/dist/src/enum-group.js +2 -2
  21. package/dist/src/erased-stores.js +317 -0
  22. package/dist/src/findings/completion.js +46 -15
  23. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/self-check.js +17 -4
  26. package/dist/src/findings-pass.js +6 -5
  27. package/dist/src/framework-lifecycle.js +1 -1
  28. package/dist/src/html-entries.js +1 -1
  29. package/dist/src/implementations.js +2 -2
  30. package/dist/src/in-operands.js +30 -0
  31. package/dist/src/indexed-access.js +264 -125
  32. package/dist/src/injection-container.js +1 -1
  33. package/dist/src/interface-satisfaction.js +2 -2
  34. package/dist/src/intra-function-parts.js +26 -12
  35. package/dist/src/inventory.js +26 -1
  36. package/dist/src/keyof-comparisons.js +170 -0
  37. package/dist/src/kinds.js +17 -7
  38. package/dist/src/manifest.js +6 -16
  39. package/dist/src/matrix.js +20 -2
  40. package/dist/src/member-names.js +1 -1
  41. package/dist/src/module-declarations.js +1 -1
  42. package/dist/src/namespace-objects.js +1 -1
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/overrides.js +2 -2
  45. package/dist/src/query.js +4 -1
  46. package/dist/src/references.js +13 -2
  47. package/dist/src/reflective-lookup.js +2 -2
  48. package/dist/src/report.js +1 -1
  49. package/dist/src/reporters/sarif.js +4 -0
  50. package/dist/src/roots.js +1 -1
  51. package/dist/src/serialization-contract.js +45 -16
  52. package/dist/src/session.js +15 -8
  53. package/dist/src/signatures.js +1 -1
  54. package/dist/src/stores.js +1 -1
  55. package/dist/src/suppress-file.js +58 -6
  56. package/dist/src/suppress-inline.js +14 -5
  57. package/dist/src/suppress.js +54 -24
  58. package/dist/src/symbol-keys.js +1 -1
  59. package/dist/src/type-errors.js +1 -1
  60. package/dist/src/type-query-alias.js +1 -1
  61. package/dist/src/unimplemented-callees.js +312 -0
  62. package/dist/src/value-flow.js +1 -1
  63. package/dist/src/value-reach.js +8 -8
  64. package/dist/src/verbs/analyze.js +11 -9
  65. package/dist/src/workspace-programs.js +2 -2
  66. package/jsr.json +1 -1
  67. package/package.json +3 -3
  68. package/src/alias-chain.ts +2 -2
  69. package/src/analysis.ts +135 -52
  70. package/src/augmentations.ts +14 -11
  71. package/src/calls.ts +2 -2
  72. package/src/class-members.ts +2 -2
  73. package/src/component-layer.ts +1 -1
  74. package/src/components.ts +103 -21
  75. package/src/configuration-files.ts +6 -5
  76. package/src/configured-declarations.ts +2 -6
  77. package/src/conformance-record.ts +1 -1
  78. package/src/contextual-uses.ts +82 -18
  79. package/src/convention-rows.ts +6 -2
  80. package/src/conventions.ts +11 -2
  81. package/src/decorator.ts +1 -1
  82. package/src/dependencies.ts +108 -31
  83. package/src/deprecation.ts +1 -1
  84. package/src/destructuring.ts +2 -2
  85. package/src/discover.ts +2 -2
  86. package/src/element-access.ts +446 -0
  87. package/src/entry-points.ts +2 -2
  88. package/src/enum-group.ts +2 -2
  89. package/src/erased-stores.ts +413 -0
  90. package/src/file-facts.ts +3 -1
  91. package/src/findings/completion.ts +54 -20
  92. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  93. package/src/findings/emitters.ts +1 -1
  94. package/src/findings/self-check.ts +18 -4
  95. package/src/findings-pass.ts +12 -5
  96. package/src/framework-lifecycle.ts +2 -2
  97. package/src/html-entries.ts +1 -1
  98. package/src/implementations.ts +2 -2
  99. package/src/in-operands.ts +45 -0
  100. package/src/indexed-access.ts +304 -146
  101. package/src/injection-container.ts +1 -1
  102. package/src/interface-satisfaction.ts +2 -2
  103. package/src/intra-function-parts.ts +42 -17
  104. package/src/inventory.ts +34 -2
  105. package/src/keyof-comparisons.ts +211 -0
  106. package/src/kinds.ts +20 -7
  107. package/src/manifest.ts +6 -21
  108. package/src/matrix.ts +34 -2
  109. package/src/member-names.ts +1 -1
  110. package/src/module-declarations.ts +1 -1
  111. package/src/namespace-objects.ts +1 -1
  112. package/src/outside-tests.ts +1 -1
  113. package/src/overrides.ts +2 -2
  114. package/src/position.ts +1 -1
  115. package/src/query.ts +12 -2
  116. package/src/references.ts +25 -3
  117. package/src/reflective-lookup.ts +2 -2
  118. package/src/report.ts +3 -1
  119. package/src/reporters/sarif.ts +4 -0
  120. package/src/roots.ts +2 -2
  121. package/src/serialization-contract.ts +55 -16
  122. package/src/session.ts +39 -32
  123. package/src/signatures.ts +1 -1
  124. package/src/source-files.ts +1 -1
  125. package/src/stores.ts +1 -1
  126. package/src/suppress-file.ts +68 -5
  127. package/src/suppress-inline.ts +15 -5
  128. package/src/suppress.ts +63 -28
  129. package/src/symbol-keys.ts +2 -2
  130. package/src/type-errors.ts +2 -2
  131. package/src/type-query-alias.ts +1 -1
  132. package/src/unimplemented-callees.ts +411 -0
  133. package/src/value-flow.ts +1 -1
  134. package/src/value-reach.ts +12 -8
  135. package/src/verbs/analyze.ts +15 -9
  136. package/src/workflow-steps.ts +1 -1
  137. package/src/workspace-programs.ts +4 -4
@@ -25,8 +25,8 @@ import {
25
25
  type Expression,
26
26
  type Node,
27
27
  type SourceFile,
28
- } from "@typescript/native/unstable/ast";
29
- import { SymbolFlags, type Type } from "@typescript/native/unstable/sync";
28
+ } from "@typescript/native/ast";
29
+ import { SymbolFlags, type Type } from "@typescript/native/sync";
30
30
  import { aliasChains } from "./alias-chain.ts";
31
31
  import { emptyImplementation } from "./implementations.ts";
32
32
  import type { DetectorInput, Evidence } from "./exempt.ts";
@@ -66,7 +66,7 @@ import {
66
66
  type Node,
67
67
  type SourceFile,
68
68
  type Statement,
69
- } from "@typescript/native/unstable/ast";
69
+ } from "@typescript/native/ast";
70
70
  import type { FindingPosition } from "./finding.ts";
71
71
  import { nodeKey, type Inventory } from "./inventory.ts";
72
72
  import { positionKey, renderPosition } from "./position.ts";
@@ -93,8 +93,17 @@ export interface PartFact {
93
93
  readonly used: boolean;
94
94
  /** Where the part is a parameter's name, what a function used as a value decides it by. */
95
95
  readonly parameter?: ParameterPlace;
96
+ /** Where the part is a result, whether its type has a `then` member, as a promise does. */
97
+ readonly thenable?: boolean;
96
98
  }
97
99
 
100
+ /**
101
+ * What a call does with its value: uses it, discards it as a statement of its own or the
102
+ * operand of `void`, or awaits it as a statement, which sequences on a thenable value and
103
+ * discards any other.
104
+ */
105
+ type CallUse = "used" | "discarded" | "awaited";
106
+
98
107
  /** What decides a parameter of a function used as a value: a caller may pass it more arguments. */
99
108
  interface ParameterPlace {
100
109
  /** Whether a parameter after this one, which is no binding-pattern name, is read. */
@@ -116,8 +125,8 @@ interface SignatureFact {
116
125
  export interface ProjectParts {
117
126
  readonly parts: readonly PartFact[];
118
127
  readonly signatures: readonly SignatureFact[];
119
- /** Each name written as the callee of a call, by position, and whether the call's value is discarded. */
120
- readonly calls: ReadonlyMap<string, boolean>;
128
+ /** Each name written as the callee of a call, by position, and what the call does with its value. */
129
+ readonly calls: ReadonlyMap<string, CallUse>;
121
130
  /** Each name an import or an export specifier writes, by position, which uses no value. */
122
131
  readonly specifiers: ReadonlySet<string>;
123
132
  }
@@ -165,16 +174,15 @@ function calleeName(call: CallExpression): Identifier | undefined {
165
174
  return isPropertyAccessExpression(callee) && isIdentifier(callee.name) ? callee.name : undefined;
166
175
  }
167
176
 
168
- /**
169
- * Whether a call's value is discarded: the call is a statement of its own, awaited as a
170
- * statement, or the operand of `void`.
171
- */
172
- function discarded(call: CallExpression): boolean {
177
+ /** What a call does with its value. */
178
+ function callUse(call: CallExpression): CallUse {
173
179
  const parent = outerParent(call);
174
180
  if (isExpressionStatement(parent) || isVoidExpression(parent)) {
175
- return true;
181
+ return "discarded";
176
182
  }
177
- return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent));
183
+ return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent))
184
+ ? "awaited"
185
+ : "used";
178
186
  }
179
187
 
180
188
  /** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
@@ -473,7 +481,7 @@ export function projectParts<Brand>(
473
481
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
474
482
  const parts: PartFact[] = [];
475
483
  const signatures: SignatureFact[] = [];
476
- const calls = new Map<string, boolean>();
484
+ const calls = new Map<string, CallUse>();
477
485
  const specifiers = new Set<string>();
478
486
  const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
479
487
  const contextual: ContextualParameter[] = [];
@@ -550,7 +558,7 @@ export function projectParts<Brand>(
550
558
  if (isCallExpression(node)) {
551
559
  const name = calleeName(node);
552
560
  if (name !== undefined) {
553
- calls.set(position(name), discarded(node));
561
+ calls.set(position(name), callUse(node));
554
562
  }
555
563
  }
556
564
  if (isImportSpecifier(node) || isExportSpecifier(node)) {
@@ -1004,10 +1012,12 @@ function resultParts<Brand>(
1004
1012
  root: string,
1005
1013
  sites: readonly { readonly file: SourceFile; readonly declared: Declared }[],
1006
1014
  references: readonly Reference[],
1007
- calls: ReadonlyMap<string, boolean>,
1015
+ calls: ReadonlyMap<string, CallUse>,
1008
1016
  ): PartFact[] {
1009
1017
  const discardedAt = new Set(
1010
- references.filter((one) => calls.get(positionKey(one.position)) === true).map((one) => one.to),
1018
+ references
1019
+ .filter((one) => (calls.get(positionKey(one.position)) ?? "used") !== "used")
1020
+ .map((one) => one.to),
1011
1021
  );
1012
1022
  const found: PartFact[] = [];
1013
1023
  for (const { file, declared } of sites) {
@@ -1030,6 +1040,8 @@ function resultParts<Brand>(
1030
1040
  continue;
1031
1041
  }
1032
1042
  const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
1043
+ const valued = project.queries.nonNullable(result);
1044
+ const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
1033
1045
  found.push({
1034
1046
  code: UNUSED_RESULT,
1035
1047
  declaration: id,
@@ -1038,6 +1050,7 @@ function resultParts<Brand>(
1038
1050
  position: spanOf(file, root, at),
1039
1051
  message: "result is discarded at every call site",
1040
1052
  used: false,
1053
+ thenable: !isAnswered(members) || members.some((member) => member.name === "then"),
1041
1054
  });
1042
1055
  }
1043
1056
  return found;
@@ -1077,7 +1090,7 @@ export function intraFunctionFacts(
1077
1090
  const known = parts.get(key);
1078
1091
  parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
1079
1092
  }
1080
- const calls = new Map<string, boolean>();
1093
+ const calls = new Map<string, CallUse>();
1081
1094
  const specifiers = new Set<string>();
1082
1095
  for (const one of projects) {
1083
1096
  for (const [at, value] of one.calls) {
@@ -1089,6 +1102,7 @@ export function intraFunctionFacts(
1089
1102
  }
1090
1103
  const valued = new Set<string>();
1091
1104
  const called = new Map<string, boolean>();
1105
+ const awaited = new Set<string>();
1092
1106
  for (const reference of references) {
1093
1107
  if (reference.use === "decorator" || reference.use === "evaluation") {
1094
1108
  continue;
@@ -1096,7 +1110,10 @@ export function intraFunctionFacts(
1096
1110
  const at = positionKey(reference.position);
1097
1111
  const call = reference.consumer === undefined ? calls.get(at) : undefined;
1098
1112
  if (call !== undefined) {
1099
- called.set(reference.to, (called.get(reference.to) ?? true) && call);
1113
+ called.set(reference.to, (called.get(reference.to) ?? true) && call !== "used");
1114
+ if (call === "awaited") {
1115
+ awaited.add(reference.to);
1116
+ }
1100
1117
  } else if (reference.consumer !== undefined || !specifiers.has(at)) {
1101
1118
  valued.add(reference.to);
1102
1119
  }
@@ -1115,8 +1132,16 @@ export function intraFunctionFacts(
1115
1132
  .map((one) => one.declaration)
1116
1133
  .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
1117
1134
  );
1135
+ // Awaiting a thenable result sequences on it, so a call that awaits one uses it.
1136
+ const sequenced = new Set(
1137
+ [...parts.values()]
1138
+ .filter((part) => part.thenable === true && awaited.has(part.declaration))
1139
+ .map((part) => part.declaration),
1140
+ );
1118
1141
  const discardedEverywhere = new Set(
1119
- [...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id),
1142
+ [...called]
1143
+ .filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
1144
+ .map(([id]) => id),
1120
1145
  );
1121
1146
  return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
1122
1147
  }
package/src/inventory.ts CHANGED
@@ -28,8 +28,8 @@ import {
28
28
  ModifierFlags,
29
29
  type Node,
30
30
  type SourceFile,
31
- } from "@typescript/native/unstable/ast";
32
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
31
+ } from "@typescript/native/ast";
32
+ import type { Symbol as TSSymbol } from "@typescript/native/sync";
33
33
  import type { Host } from "./host.ts";
34
34
  import { dirnamePath, joinPath, relativePath } from "./paths.ts";
35
35
  import {
@@ -165,6 +165,12 @@ export interface Inventory {
165
165
  readonly byName: ReadonlyMap<string, readonly string[]>;
166
166
  /** Each `declare module` declaration whose name is a string literal, by id, to that name. */
167
167
  readonly modules: ReadonlyMap<string, string>;
168
+ /**
169
+ * Each enum member's dependents, by id: the later members of its declaration with no
170
+ * initializer, up to the first one with an initializer, whose values its deletion moves.
171
+ * A member with none has no entry.
172
+ */
173
+ readonly dependents: ReadonlyMap<string, readonly string[]>;
168
174
  }
169
175
 
170
176
  /** A declaration under construction, before its reference is rendered. */
@@ -986,6 +992,31 @@ export function inventory<Brand>(
986
992
  return undefined;
987
993
  }
988
994
 
995
+ const dependents = new Map<string, readonly string[]>();
996
+ for (const container of containers.filter((one) => one.memberKind === "enum-member")) {
997
+ const ids = container.members.map((member) => {
998
+ const record = byNode.get(nodeKey(member.getSourceFile(), member));
999
+ return record === undefined ? undefined : positionKey(record.position);
1000
+ });
1001
+ container.members.forEach((_member, index) => {
1002
+ const after: string[] = [];
1003
+ for (let next = index + 1; next < container.members.length; next += 1) {
1004
+ const member = container.members[next];
1005
+ if (member === undefined || !isEnumMember(member) || member.initializer !== undefined) {
1006
+ break;
1007
+ }
1008
+ const id = ids[next];
1009
+ if (id !== undefined) {
1010
+ after.push(id);
1011
+ }
1012
+ }
1013
+ const id = ids[index];
1014
+ if (id !== undefined && after.length > 0) {
1015
+ dependents.set(id, after);
1016
+ }
1017
+ });
1018
+ }
1019
+
989
1020
  const byId = new Map(building.map((record) => [positionKey(record.position), record]));
990
1021
  const symbols = building
991
1022
  .map(render)
@@ -1002,6 +1033,7 @@ export function inventory<Brand>(
1002
1033
  .map((symbol) => symbol.id),
1003
1034
  byName: namesOf(building),
1004
1035
  modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
1036
+ dependents,
1005
1037
  };
1006
1038
  }
1007
1039
 
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The members a comparison with a key reads: where one operand of `===`, `!==`, `==` or
3
+ * `!=`, or a `switch` expression a `case` label is compared with, is declared with a type
4
+ * that holds `keyof X`, the literal the other side has names the member of `X` under that
5
+ * key, because deleting the member makes the comparison a type error.
6
+ */
7
+
8
+ import {
9
+ isBinaryExpression,
10
+ isCaseClause,
11
+ isElementAccessExpression,
12
+ isIdentifier,
13
+ isParenthesizedExpression,
14
+ isPropertyAccessExpression,
15
+ isSwitchStatement,
16
+ isTypeOperatorNode,
17
+ SyntaxKind,
18
+ type Expression,
19
+ type Node,
20
+ type SourceFile,
21
+ type TypeNode,
22
+ } from "@typescript/native/ast";
23
+ import { SymbolFlags, type Type } from "@typescript/native/sync";
24
+ import type { AliasChains } from "./alias-chain.ts";
25
+ import { nodeKey, type Inventory } from "./inventory.ts";
26
+ import { renderPosition } from "./position.ts";
27
+ import { UNANSWERED } from "./query.ts";
28
+ import type { Reference } from "./references.ts";
29
+ import type { ProjectView } from "./session.ts";
30
+
31
+ /** The operators that compare their operands for equality. */
32
+ const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
33
+ SyntaxKind.EqualsEqualsEqualsToken,
34
+ SyntaxKind.ExclamationEqualsEqualsToken,
35
+ SyntaxKind.EqualsEqualsToken,
36
+ SyntaxKind.ExclamationEqualsToken,
37
+ ]);
38
+
39
+ /** One comparison of a keyed operand with another, and the declaration that writes it. */
40
+ interface Comparison {
41
+ readonly file: SourceFile;
42
+ readonly from: string;
43
+ readonly keyed: Expression;
44
+ readonly other: Expression;
45
+ }
46
+
47
+ function unwrapped(node: Expression): Expression {
48
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
49
+ }
50
+
51
+ /** The name node an operand's symbol is read at: an identifier, or a property access's name. */
52
+ function nameOf(node: Expression): Node | undefined {
53
+ const at = unwrapped(node);
54
+ if (isIdentifier(at)) {
55
+ return at;
56
+ }
57
+ return isPropertyAccessExpression(at) ? at.name : undefined;
58
+ }
59
+
60
+ /** The operand of each `keyof` one type node holds, through every type node it is built of. */
61
+ function keyofOperands(node: TypeNode): readonly TypeNode[] {
62
+ const found: TypeNode[] = [];
63
+ const visit = (at: Node): void => {
64
+ if (isTypeOperatorNode(at) && at.operator === SyntaxKind.KeyOfKeyword) {
65
+ found.push(at.type);
66
+ return;
67
+ }
68
+ at.forEachChild(visit);
69
+ };
70
+ visit(node);
71
+ return found;
72
+ }
73
+
74
+ /** The names a literal type or a union of them spells, or none. */
75
+ function literalNames(parts: readonly Type[]): readonly string[] {
76
+ const names: string[] = [];
77
+ for (const part of parts) {
78
+ if (part.isStringLiteralType()) {
79
+ names.push(part.value);
80
+ } else if (part.isNumberLiteralType()) {
81
+ names.push(String(part.value));
82
+ } else {
83
+ return [];
84
+ }
85
+ }
86
+ return names;
87
+ }
88
+
89
+ /**
90
+ * The references the key comparisons of one project's own files make to the members their
91
+ * literal side names, each made by the declaration holding the comparison.
92
+ */
93
+ export function keyofComparisonReferences<Brand>(
94
+ project: ProjectView<Brand>,
95
+ held: Inventory,
96
+ chains: AliasChains,
97
+ root: string,
98
+ testFiles: ReadonlySet<string>,
99
+ ): readonly Reference[] {
100
+ const queries = project.queries;
101
+ const comparisons: Comparison[] = [];
102
+ for (const file of project.ownSourceFiles()) {
103
+ const fileId = held.declarations.get(nodeKey(file, file));
104
+ if (fileId === undefined) {
105
+ continue;
106
+ }
107
+ const visit = (node: Node, from: string): void => {
108
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
109
+ if (isBinaryExpression(node) && EQUALITIES.has(node.operatorToken.kind)) {
110
+ comparisons.push({ file, from: holder, keyed: node.left, other: node.right });
111
+ comparisons.push({ file, from: holder, keyed: node.right, other: node.left });
112
+ }
113
+ if (isSwitchStatement(node)) {
114
+ for (const clause of node.caseBlock.clauses) {
115
+ if (isCaseClause(clause)) {
116
+ comparisons.push({
117
+ file,
118
+ from: holder,
119
+ keyed: node.expression,
120
+ other: clause.expression,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ node.forEachChild((child) => {
126
+ visit(child, holder);
127
+ });
128
+ };
129
+ file.forEachChild((child) => {
130
+ visit(child, fileId);
131
+ });
132
+ }
133
+ const named = comparisons.flatMap((one) => {
134
+ const name = nameOf(one.keyed);
135
+ return name === undefined || isElementAccessExpression(unwrapped(one.other))
136
+ ? []
137
+ : [{ ...one, name }];
138
+ });
139
+ if (named.length === 0) {
140
+ return [];
141
+ }
142
+
143
+ // The operand's own declaration states the key type, which the checker resolves to
144
+ // the literals it admits and so no longer names the type the key indexes.
145
+ const symbols = queries.symbolsAt(named.map((one) => one.name));
146
+ const keyed = named.flatMap((one, at) => {
147
+ const asked = symbols[at];
148
+ const symbol =
149
+ asked === UNANSWERED || asked === undefined || (asked.flags & SymbolFlags.Alias) === 0
150
+ ? asked
151
+ : queries.aliased(asked);
152
+ if (symbol === UNANSWERED || symbol === undefined) {
153
+ return [];
154
+ }
155
+ const operands = symbol.declarations.flatMap((handle) => {
156
+ const declared = (
157
+ project.declarationAt(handle)?.node as { readonly type?: TypeNode } | undefined
158
+ )?.type;
159
+ return declared === undefined ? [] : keyofOperands(declared);
160
+ });
161
+ return operands.length === 0 ? [] : [{ ...one, operands }];
162
+ });
163
+ if (keyed.length === 0) {
164
+ return [];
165
+ }
166
+ const nodes = keyed.flatMap((one) => [one.other, ...one.operands]);
167
+ const types = queries.typesAt(nodes);
168
+ const typeOf = new Map<Node, Type>();
169
+ nodes.forEach((node, at) => {
170
+ const type = types[at];
171
+ if (type !== UNANSWERED && type !== undefined) {
172
+ typeOf.set(node, type);
173
+ }
174
+ });
175
+ const partsOf = (type: Type): readonly Type[] => {
176
+ const parts = queries.constituents(type);
177
+ return parts === UNANSWERED ? [type] : parts;
178
+ };
179
+
180
+ const found: Reference[] = [];
181
+ for (const one of keyed) {
182
+ const literal = typeOf.get(one.other);
183
+ const names = literal === undefined ? [] : literalNames(partsOf(literal));
184
+ if (names.length === 0) {
185
+ continue;
186
+ }
187
+ const position = renderPosition(one.file, root, one.other.getStart());
188
+ for (const operand of one.operands) {
189
+ const object = typeOf.get(operand);
190
+ for (const part of object === undefined ? [] : partsOf(object)) {
191
+ const properties = queries.propertiesOf(part);
192
+ for (const property of properties === UNANSWERED ? [] : properties) {
193
+ if (!names.includes(property.name)) {
194
+ continue;
195
+ }
196
+ for (const to of chains.declarationsOf(property)) {
197
+ found.push({
198
+ from: one.from,
199
+ to,
200
+ position,
201
+ use: "read",
202
+ resolution: "keyed",
203
+ test: testFiles.has(position.path),
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ return found;
211
+ }
package/src/kinds.ts CHANGED
@@ -19,6 +19,8 @@ export interface KindRow {
19
19
  /** The highest confidence a finding of the kind carries. */
20
20
  readonly maxClass: Confidence;
21
21
  readonly fixability: Fixability;
22
+ /** Each fixability a finding of the kind may carry in place of `fixability`. */
23
+ readonly findingFixabilities?: readonly Fixability[];
22
24
  /**
23
25
  * Whether the Contract fixes the kind on at its default severity, so a severity key
24
26
  * naming the code, or a family prefix whose range holds it, is an unimplemented key.
@@ -43,6 +45,7 @@ const ROWS: readonly KindRow[] = [
43
45
  defaultSeverity: "deny",
44
46
  maxClass: "certain",
45
47
  fixability: "deletable",
48
+ findingFixabilities: ["manual"],
46
49
  fixed: false,
47
50
  },
48
51
  {
@@ -53,6 +56,7 @@ const ROWS: readonly KindRow[] = [
53
56
  defaultSeverity: "deny",
54
57
  maxClass: "certain",
55
58
  fixability: "deletable",
59
+ findingFixabilities: ["manual"],
56
60
  fixed: false,
57
61
  },
58
62
  {
@@ -65,6 +69,7 @@ const ROWS: readonly KindRow[] = [
65
69
  defaultSeverity: "deny",
66
70
  maxClass: "certain",
67
71
  fixability: "deletable",
72
+ findingFixabilities: ["manual"],
68
73
  fixed: false,
69
74
  },
70
75
  {
@@ -75,6 +80,7 @@ const ROWS: readonly KindRow[] = [
75
80
  defaultSeverity: "deny",
76
81
  maxClass: "certain",
77
82
  fixability: "deletable",
83
+ findingFixabilities: ["manual"],
78
84
  fixed: false,
79
85
  },
80
86
  {
@@ -85,6 +91,7 @@ const ROWS: readonly KindRow[] = [
85
91
  defaultSeverity: "deny",
86
92
  maxClass: "certain",
87
93
  fixability: "deletable",
94
+ findingFixabilities: ["manual"],
88
95
  fixed: false,
89
96
  },
90
97
  {
@@ -95,6 +102,7 @@ const ROWS: readonly KindRow[] = [
95
102
  defaultSeverity: "deny",
96
103
  maxClass: "certain",
97
104
  fixability: "deletable",
105
+ findingFixabilities: ["manual"],
98
106
  fixed: false,
99
107
  },
100
108
  {
@@ -103,7 +111,7 @@ const ROWS: readonly KindRow[] = [
103
111
  languages: ["go", "ts"],
104
112
  rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
105
113
  precondition:
106
- "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
114
+ "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name. A package-level declaration whose name a string literal spells carries the class grammar/analysis.md gives it.",
107
115
  defaultSeverity: "warn",
108
116
  maxClass: "certain",
109
117
  fixability: "narrowable",
@@ -125,10 +133,11 @@ const ROWS: readonly KindRow[] = [
125
133
  code: "DS1103",
126
134
  name: "unreachable-export",
127
135
  languages: ["go", "ts"],
128
- rule: "An unused exported symbol in a package or file no external code can import: an unused-exported candidate whose enclosing package or file is unimportable from outside, reported once under this code at the certain class, because unimportability is a property of the package graph rather than of the consumer set.",
136
+ rule: "An unused exported symbol in a package or file no external code can import: an unused-exported candidate whose enclosing package or file is unimportable from outside, reported once under this code at the certain class, because unimportability is a property of the package graph rather than of the consumer set, except where the confidence rule of grammar/analysis.md gives a declaration a string literal spells the class possible.",
129
137
  defaultSeverity: "deny",
130
138
  maxClass: "certain",
131
139
  fixability: "deletable",
140
+ findingFixabilities: ["manual"],
132
141
  fixed: false,
133
142
  },
134
143
  {
@@ -151,6 +160,7 @@ const ROWS: readonly KindRow[] = [
151
160
  defaultSeverity: "deny",
152
161
  maxClass: "certain",
153
162
  fixability: "deletable",
163
+ findingFixabilities: ["manual"],
154
164
  fixed: false,
155
165
  },
156
166
  {
@@ -170,6 +180,8 @@ const ROWS: readonly KindRow[] = [
170
180
  name: "unused-satisfaction-assertion",
171
181
  languages: ["go"],
172
182
  rule: "A compile-time satisfaction assertion whose interface no symbol uses as a type.",
183
+ precondition:
184
+ "In a library, an assertion whose interface is published API, an exported interface of a package outside programs can import, has the reachability class possible, whatever consumers the run declares and loads. The assertion checks a promise the published API makes, that the type satisfies the published interface, and that promise is made to every program that imports the package, not only to the consumers the run declares. A loaded consumer that names the interface as a type uses it, and the assertion is not reported.",
173
185
  defaultSeverity: "deny",
174
186
  maxClass: "certain",
175
187
  fixability: "deletable",
@@ -179,7 +191,7 @@ const ROWS: readonly KindRow[] = [
179
191
  code: "DS1301",
180
192
  name: "write-only-symbol",
181
193
  languages: ["go", "ts"],
182
- rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
194
+ rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
183
195
  defaultSeverity: "deny",
184
196
  maxClass: "certain",
185
197
  fixability: "deletable",
@@ -193,6 +205,7 @@ const ROWS: readonly KindRow[] = [
193
205
  defaultSeverity: "deny",
194
206
  maxClass: "certain",
195
207
  fixability: "deletable",
208
+ findingFixabilities: ["manual"],
196
209
  fixed: false,
197
210
  },
198
211
  {
@@ -211,7 +224,7 @@ const ROWS: readonly KindRow[] = [
211
224
  code: "DS1501",
212
225
  name: "file-never-built",
213
226
  languages: ["go", "ts"],
214
- rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint is the ignore tag is never reported: the toolchain applies no build constraint to a file named on its own command line, so that tag is the toolchain's convention for a file built by hand. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
227
+ rule: "A source file no configuration in the build matrix builds. On the Go side the finding names the build constraint that excluded the file. A file whose build constraint holds only when the ignore tag is set is never reported, because grammar/analysis.md keeps such a file outside the analysis wherever no configuration builds it: the tag is the toolchain's convention for a file kept out of ordinary package builds unless a configuration explicitly satisfies the tag, and built by hand, the toolchain applying no build constraint to a file named on its own command line. A file any other custom tag excludes is reported under a matrix the configuration declares complete, because completeness is the maintainer's assertion that the listed configurations are every one the target builds, and a configuration the maintainer builds by hand belongs in that list.",
215
228
  precondition:
216
229
  'Reported only when the configuration declares the build matrix complete, and never for a file the toolchain ignored solely because it imports "C" under a build with cgo disabled. Such a file is recorded as excluded by cgo rather than as never built.',
217
230
  defaultSeverity: "deny",
@@ -235,7 +248,7 @@ const ROWS: readonly KindRow[] = [
235
248
  languages: ["go", "ts"],
236
249
  rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
237
250
  precondition:
238
- "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs. Three more dependencies are needed: one whose command a script entry or a workflow step runs, one that a string of a configuration file names, and one that a string names under a manifest key or a package key of an applied convention row, each as grammar/analysis.md states. A command no script entry and no workflow step runs needs nothing, and a peer the installed manifest of a needed dependency requires is needed only as any other dependency is, because the package manager installs a required peer itself. A dependency of a manifest whose directory holds source only in configurations the run lists in configurations_not_built is reported at the reachability class possible, because the source that may import it was not analyzed. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
251
+ "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs. Four more dependencies are needed: one whose command a script entry or a workflow step runs, one that a string of a configuration file names, and one that a string names under a manifest key or a package key of an applied convention row, each as grammar/analysis.md states, and one that a needed dependency names as a peer, as stated below. A command no script entry and no workflow step runs needs nothing. A declared dependency that the installed manifest of a needed declared dependency names in its peerDependencies is needed, and so on until no further dependency joins. A peer dependency is a package the needed dependency expects beside it at a version its manifest states, whether or not it imports the package, and the project's declaration is what keeps that package present at a version the project chooses, so removing the declaration can break the needed dependency where nothing the analysis reads imports the peer. A peer that peerDependenciesMeta marks optional is needed in the same way, because the needed dependency's manifest still names it as a package it works with, and a package manager may leave an optional peer out where the project does not declare it, so the project's declaration is what keeps it present for the needed dependency. A declared dependency that only dependencies nothing needs name as a peer is needed by nothing. A dependency of a manifest whose directory holds source only in configurations the run lists in configurations_not_built is reported at the reachability class possible, because the source that may import it was not analyzed. An installed manifest is the package's manifest in the nearest node_modules directory at or above the directory of the manifest that declares the dependency, and a dependency with none is decided by the import closure alone. On the TypeScript side, the dependencies judged are those of every manifest grammar/analysis.md says the analysis reads, a nested package's among them, each read against its own directory: a dependency is needed by a use written in a file at or below the directory of the manifest that declares it, by a use no one file holds, and as a peer of a needed dependency of the same manifest or of a manifest below it. A dependency a nested package's manifest declares whose installed manifest the analysis cannot find is not judged, because no run requires a nested package to be installed and its commands and peers cannot be read, while a workspace member's and the target's own are judged as above.",
239
252
  defaultSeverity: "deny",
240
253
  maxClass: "certain",
241
254
  fixability: "manual",
@@ -277,7 +290,7 @@ const ROWS: readonly KindRow[] = [
277
290
  code: "DS1703",
278
291
  name: "stale-suppression",
279
292
  languages: ["go", "ts"],
280
- rule: "A suppression that matches no current finding, at either mechanism and in the baseline alike. The run exits with the findings code when at least one is reported. The kind is fixed on at deny: no flag, no severity setting and no per-mechanism exception reduces it below a finding, and a configuration naming this code under a severity key is an unimplemented key.",
293
+ rule: "A suppression record that is stale, as grammar/suppression.md defines it, at either mechanism and in the baseline alike. The run exits with the findings code when at least one is reported. The kind is fixed on at deny: no flag, no severity setting and no per-mechanism exception reduces it below a finding, and a configuration naming this code under a severity key is an unimplemented key.",
281
294
  defaultSeverity: "deny",
282
295
  maxClass: "certain",
283
296
  fixability: "none",
@@ -345,7 +358,7 @@ const ROWS: readonly KindRow[] = [
345
358
  languages: ["go", "ts"],
346
359
  rule: "A result value that no call site of the function uses, on a function whose signature is free to change.",
347
360
  precondition:
348
- "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible.",
361
+ "The same free-signature rule as unused-parameter, plus a closed-world condition: every call site of the function must be in the loaded graph, because an unknown caller may consume the result, so the kind reports only for a function whose callers are all visible. A call that is the operand of an await expression written as a statement of its own uses a result whose type has a member named then, a promise or another thenable, because the caller sequences on it, and discards any other result. A call written as a statement of its own, or as the operand of void, discards its result whatever its type.",
349
362
  defaultSeverity: "warn",
350
363
  maxClass: "certain",
351
364
  fixability: "manual",
package/src/manifest.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Host } from "./host.ts";
9
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
9
+ import { isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
 
11
11
  /** The manifest's own name. */
12
12
  const MANIFEST = "package.json";
@@ -212,31 +212,16 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
212
212
  }
213
213
 
214
214
  /**
215
- * The entry points every package manifest between the target root and each compiler
216
- * configuration names: each directory holding a `package.json` that a configuration sits
217
- * in or below, the target root and `known` excluded, whose manifests the run reads
218
- * otherwise. Each entry is spelled under its manifest's path below the target root, and
219
- * is published by its own manifest's rule, as the target's own entries are.
215
+ * The entry points the manifests of the nested packages in `dirs` name. Each entry is
216
+ * spelled under its manifest's path below the target root, and is published by its own
217
+ * manifest's rule, as the target's own entries are.
220
218
  */
221
219
  export function packageEntries(
222
220
  host: Host,
223
221
  targetRoot: string,
224
- configFiles: readonly string[],
225
- known: ReadonlySet<string>,
222
+ dirs: readonly string[],
226
223
  ): readonly ManifestEntry[] {
227
- const dirs = new Set<string>();
228
- for (const configFile of configFiles) {
229
- for (
230
- let dir = dirnamePath(resolvePath(targetRoot, configFile));
231
- dir !== targetRoot && relativePath(targetRoot, dir) !== undefined;
232
- dir = dirnamePath(dir)
233
- ) {
234
- if (!known.has(dir) && host.kindOf(joinPath(dir, MANIFEST)) === "file") {
235
- dirs.add(dir);
236
- }
237
- }
238
- }
239
- return [...dirs].sort().flatMap((dir) => {
224
+ return dirs.flatMap((dir) => {
240
225
  const manifest = readManifest(host, dir);
241
226
  const where = `${relativePath(targetRoot, dir) ?? dir}/${MANIFEST}`;
242
227
  return manifest.entries.map((entry) => ({