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

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 (75) hide show
  1. package/dist/src/analysis.js +89 -44
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/components.js +62 -20
  4. package/dist/src/configuration-files.js +5 -5
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +71 -14
  7. package/dist/src/convention-rows.js +6 -2
  8. package/dist/src/conventions.js +3 -1
  9. package/dist/src/dependencies.js +80 -26
  10. package/dist/src/element-access.js +341 -0
  11. package/dist/src/erased-stores.js +317 -0
  12. package/dist/src/findings/completion.js +46 -15
  13. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  14. package/dist/src/findings/emitters.js +1 -1
  15. package/dist/src/findings/self-check.js +17 -4
  16. package/dist/src/findings-pass.js +6 -5
  17. package/dist/src/in-operands.js +30 -0
  18. package/dist/src/indexed-access.js +264 -125
  19. package/dist/src/intra-function-parts.js +25 -11
  20. package/dist/src/inventory.js +25 -0
  21. package/dist/src/keyof-comparisons.js +170 -0
  22. package/dist/src/kinds.js +17 -7
  23. package/dist/src/manifest.js +6 -16
  24. package/dist/src/matrix.js +20 -2
  25. package/dist/src/query.js +3 -0
  26. package/dist/src/references.js +12 -1
  27. package/dist/src/report.js +1 -1
  28. package/dist/src/reporters/sarif.js +4 -0
  29. package/dist/src/serialization-contract.js +43 -14
  30. package/dist/src/session.js +12 -7
  31. package/dist/src/suppress-file.js +58 -6
  32. package/dist/src/suppress-inline.js +13 -4
  33. package/dist/src/suppress.js +54 -24
  34. package/dist/src/unimplemented-callees.js +312 -0
  35. package/dist/src/value-reach.js +7 -7
  36. package/dist/src/verbs/analyze.js +11 -9
  37. package/jsr.json +1 -1
  38. package/package.json +2 -2
  39. package/src/analysis.ts +135 -52
  40. package/src/augmentations.ts +14 -11
  41. package/src/components.ts +103 -21
  42. package/src/configuration-files.ts +5 -4
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contextual-uses.ts +81 -17
  45. package/src/convention-rows.ts +6 -2
  46. package/src/conventions.ts +10 -1
  47. package/src/dependencies.ts +107 -30
  48. package/src/element-access.ts +446 -0
  49. package/src/erased-stores.ts +413 -0
  50. package/src/file-facts.ts +2 -0
  51. package/src/findings/completion.ts +54 -20
  52. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  53. package/src/findings/emitters.ts +1 -1
  54. package/src/findings/self-check.ts +18 -4
  55. package/src/findings-pass.ts +12 -5
  56. package/src/in-operands.ts +45 -0
  57. package/src/indexed-access.ts +303 -145
  58. package/src/intra-function-parts.ts +41 -16
  59. package/src/inventory.ts +32 -0
  60. package/src/keyof-comparisons.ts +211 -0
  61. package/src/kinds.ts +20 -7
  62. package/src/manifest.ts +6 -21
  63. package/src/matrix.ts +34 -2
  64. package/src/query.ts +10 -0
  65. package/src/references.ts +23 -1
  66. package/src/report.ts +3 -1
  67. package/src/reporters/sarif.ts +4 -0
  68. package/src/serialization-contract.ts +53 -14
  69. package/src/session.ts +34 -29
  70. package/src/suppress-file.ts +68 -5
  71. package/src/suppress-inline.ts +14 -4
  72. package/src/suppress.ts +63 -28
  73. package/src/unimplemented-callees.ts +411 -0
  74. package/src/value-reach.ts +11 -7
  75. package/src/verbs/analyze.ts +15 -9
@@ -0,0 +1,317 @@
1
+ /**
2
+ * The values the program stores in a slot of a container: a property, an index signature,
3
+ * an array element, and a variable. A serializer walking the container emits the value the
4
+ * slot holds, so it walks each stored value at its own static type, a subtype stored where
5
+ * the slot's type names a supertype, `object`, `unknown` or `any` included. A container is
6
+ * identified by the checker's type identity, so two containers of one type share what
7
+ * either stores. A store into an erased property of a type the analyzed program does not
8
+ * declare hands the value to the code that declares the type.
9
+ */
10
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isElementAccessExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, SyntaxKind, } from "@typescript/native/unstable/ast";
11
+ import { SymbolFlags, TypeFlags } from "@typescript/native/unstable/sync";
12
+ import { must } from "./query.js";
13
+ /** The types that keep nothing of the value they are given. */
14
+ const ERASED = TypeFlags.Any | TypeFlags.Unknown;
15
+ /** The array methods whose arguments become elements of the array they are called on. */
16
+ const ELEMENT_STORES = new Set(["push", "unshift"]);
17
+ /** The name an element access's literal index spells, or none. */
18
+ function literalKey(node) {
19
+ return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node) || isNumericLiteral(node)
20
+ ? node.text
21
+ : undefined;
22
+ }
23
+ function unwrapped(node) {
24
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
25
+ }
26
+ /**
27
+ * Whether a variable declaration's type can differ from what is stored in it: it carries an
28
+ * annotation, or it has neither an annotation nor an initializer, so its type evolves.
29
+ */
30
+ function declaresSlot(node) {
31
+ const held = node;
32
+ return held.type !== undefined || held.initializer === undefined;
33
+ }
34
+ /** The element of an array literal at one place, where no spread before it moves the places. */
35
+ function elementAt(source, at) {
36
+ if (!isArrayLiteralExpression(source)) {
37
+ return undefined;
38
+ }
39
+ const before = source.elements.slice(0, at + 1);
40
+ return before.some((one) => isSpreadElement(one)) ? undefined : source.elements[at];
41
+ }
42
+ /** The name a property spells, and none for a computed one. */
43
+ function propertyName(name) {
44
+ return isIdentifier(name) || isStringLiteral(name) || isNumericLiteral(name)
45
+ ? name.text
46
+ : undefined;
47
+ }
48
+ /** The name one property of an object literal spells and the expression it holds, or none. */
49
+ function propertyParts(property) {
50
+ if (isPropertyAssignment(property)) {
51
+ const name = propertyName(property.name);
52
+ return name === undefined ? undefined : { name, value: property.initializer };
53
+ }
54
+ if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
55
+ return { name: property.name.text, value: property.name };
56
+ }
57
+ return undefined;
58
+ }
59
+ /** The value an object literal gives one property, where it gives it by a plain or a shorthand property. */
60
+ function propertyAt(source, name) {
61
+ if (!isObjectLiteralExpression(source)) {
62
+ return undefined;
63
+ }
64
+ for (const property of source.properties) {
65
+ const parts = propertyParts(property);
66
+ if (parts?.name === name) {
67
+ return parts.value;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Every target one destructuring pattern writes at any depth, each with the expression
74
+ * whose value it receives: the matching element or property of a literal on the right,
75
+ * else the right side with the path into it. A rest element copies, so it is no target
76
+ * here, and a default value is stored in its target too.
77
+ */
78
+ function patternTargets(pattern, from, out) {
79
+ const target = unwrapped(pattern);
80
+ const source = from.path.length === 0 ? unwrapped(from.value) : from.value;
81
+ const into = (element, step, literal) => {
82
+ patternTargets(element, literal === undefined
83
+ ? { value: from.value, path: [...from.path, step] }
84
+ : { value: literal, path: [] }, out);
85
+ };
86
+ if (isArrayLiteralExpression(target)) {
87
+ target.elements.forEach((element, at) => {
88
+ if (!isSpreadElement(element) && element.kind !== SyntaxKind.OmittedExpression) {
89
+ into(element, at, from.path.length === 0 ? elementAt(source, at) : undefined);
90
+ }
91
+ });
92
+ return;
93
+ }
94
+ if (isObjectLiteralExpression(target)) {
95
+ for (const property of target.properties) {
96
+ const parts = propertyParts(property);
97
+ if (parts !== undefined) {
98
+ into(parts.value, parts.name, from.path.length === 0 ? propertyAt(source, parts.name) : undefined);
99
+ }
100
+ if (parts !== undefined &&
101
+ isShorthandPropertyAssignment(property) &&
102
+ property.objectAssignmentInitializer !== undefined) {
103
+ out.push({ target: parts.value, value: property.objectAssignmentInitializer, path: [] });
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ if (isBinaryExpression(target) && target.operatorToken.kind === SyntaxKind.EqualsToken) {
109
+ patternTargets(target.left, from, out);
110
+ out.push({ target: target.left, value: target.right, path: [] });
111
+ return;
112
+ }
113
+ out.push({ target, ...from });
114
+ }
115
+ /** Every store one project's own files write, and the stores into variables declared erased. */
116
+ export function erasedStores(project) {
117
+ const queries = project.queries;
118
+ const own = project.ownPaths();
119
+ const stores = [];
120
+ const assigned = [];
121
+ const slotVariables = [];
122
+ for (const file of project.ownSourceFiles()) {
123
+ const visit = (node) => {
124
+ if (isObjectLiteralExpression(node)) {
125
+ for (const property of node.properties) {
126
+ if (isShorthandPropertyAssignment(property) && isIdentifier(property.name)) {
127
+ const { name } = property;
128
+ stores.push({
129
+ container: node,
130
+ contextual: true,
131
+ name: name.text,
132
+ value: name,
133
+ path: [],
134
+ });
135
+ }
136
+ else if (isPropertyAssignment(property) &&
137
+ (isIdentifier(property.name) || isStringLiteral(property.name))) {
138
+ const value = property.initializer;
139
+ stores.push({
140
+ container: node,
141
+ contextual: true,
142
+ name: property.name.text,
143
+ value,
144
+ path: [],
145
+ });
146
+ }
147
+ }
148
+ }
149
+ else if (isArrayLiteralExpression(node)) {
150
+ for (const element of node.elements) {
151
+ if (!isSpreadElement(element)) {
152
+ stores.push({
153
+ container: node,
154
+ contextual: true,
155
+ name: undefined,
156
+ value: element,
157
+ path: [],
158
+ });
159
+ }
160
+ }
161
+ }
162
+ else if (isBinaryExpression(node) && node.operatorToken.kind === SyntaxKind.EqualsToken) {
163
+ const targets = [];
164
+ patternTargets(node.left, { value: node.right, path: [] }, targets);
165
+ for (const { target, value, path } of targets) {
166
+ if (isPropertyAccessExpression(target)) {
167
+ stores.push({
168
+ container: target.expression,
169
+ contextual: false,
170
+ name: target.name.text,
171
+ value,
172
+ path,
173
+ });
174
+ }
175
+ else if (isElementAccessExpression(target)) {
176
+ stores.push({
177
+ container: target.expression,
178
+ contextual: false,
179
+ name: literalKey(target.argumentExpression),
180
+ value,
181
+ path,
182
+ });
183
+ }
184
+ else if (isIdentifier(target)) {
185
+ assigned.push({ target, value, path });
186
+ }
187
+ }
188
+ }
189
+ else if (isCallExpression(node) &&
190
+ isPropertyAccessExpression(node.expression) &&
191
+ ELEMENT_STORES.has(node.expression.name.text)) {
192
+ for (const argument of node.arguments) {
193
+ stores.push({
194
+ container: node.expression.expression,
195
+ contextual: false,
196
+ name: undefined,
197
+ value: argument,
198
+ path: [],
199
+ });
200
+ }
201
+ }
202
+ else if (isVariableDeclaration(node) && declaresSlot(node)) {
203
+ slotVariables.push(node);
204
+ if (node.initializer !== undefined) {
205
+ assigned.push({ target: node, value: node.initializer, path: [] });
206
+ }
207
+ }
208
+ node.forEachChild(visit);
209
+ };
210
+ file.forEachChild(visit);
211
+ }
212
+ const contextual = stores.filter((one) => one.contextual);
213
+ const contexts = queries.contextualTypes(contextual.map((one) => one.container)).map(must);
214
+ const containerOf = new Map(contextual.map((one, at) => [one, contexts[at]]));
215
+ const typed = stores.filter((one) => !one.contextual);
216
+ queries
217
+ .typesAt(typed.map((one) => one.container))
218
+ .map(must)
219
+ .forEach((type, at) => {
220
+ const one = typed[at];
221
+ if (one !== undefined) {
222
+ containerOf.set(one, type);
223
+ }
224
+ });
225
+ const slotTypes = new Map();
226
+ /** The type a slot of one object type holds: a property's, else the string index's, else an element's. */
227
+ const slotOf = (type, name) => {
228
+ const key = `${String(type.id)}\u0000${name ?? ""}`;
229
+ if (slotTypes.has(key)) {
230
+ return slotTypes.get(key);
231
+ }
232
+ let found;
233
+ const property = name === undefined
234
+ ? undefined
235
+ : must(queries.propertiesOf(type)).find((one) => one.name === name);
236
+ if (property !== undefined) {
237
+ found =
238
+ (property.flags & SymbolFlags.Property) !== 0
239
+ ? must(queries.typeOfSymbol(property))
240
+ : undefined;
241
+ }
242
+ else if (type.isTypeReference() && must(queries.isArray(type))) {
243
+ found = must(queries.typeArguments(type))[0];
244
+ }
245
+ else {
246
+ found = must(queries.indexInfosOf(type))[0]?.valueType;
247
+ }
248
+ slotTypes.set(key, found);
249
+ return found;
250
+ };
251
+ const declaredOutside = (type) => {
252
+ const symbol = must(queries.symbolOfType(type));
253
+ return (symbol !== undefined &&
254
+ symbol.declarations.length > 0 &&
255
+ symbol.declarations.every((handle) => !own.has(handle.path)));
256
+ };
257
+ /** The type of the part of a stored value its path names, a step at a time. */
258
+ const partOf = (type, path) => path.reduce((at, step) => (at === undefined ? undefined : slotOf(at, String(step))), type);
259
+ const valueTypes = queries
260
+ .typesAt(stores.map((one) => one.value))
261
+ .map((type, at) => partOf(must(type), stores[at]?.path ?? []));
262
+ const inside = new Map();
263
+ const outside = [];
264
+ stores.forEach((store, at) => {
265
+ const container = containerOf.get(store);
266
+ const value = valueTypes[at];
267
+ if (container === undefined || value === undefined) {
268
+ return;
269
+ }
270
+ const parts = container.isUnionType() || container.isIntersectionType()
271
+ ? must(queries.constituents(container))
272
+ : [container];
273
+ for (const part of parts) {
274
+ if (!part.isObjectType()) {
275
+ continue;
276
+ }
277
+ const { name } = store;
278
+ const erased = name !== undefined && ((slotOf(part, name)?.flags ?? 0) & ERASED) !== 0;
279
+ if (erased && declaredOutside(part)) {
280
+ outside.push({ value: store.value, type: value, property: name });
281
+ }
282
+ else {
283
+ inside.set(part.id, [...(inside.get(part.id) ?? []), value]);
284
+ }
285
+ }
286
+ });
287
+ const targets = assigned.map((one) => isVariableDeclaration(one.target) ? one.target.name : one.target);
288
+ const symbols = queries.symbolsAt(targets).map(must);
289
+ const slots = new Set(slotVariables);
290
+ const declarationOf = (symbol) => {
291
+ const handle = symbol?.valueDeclaration ?? symbol?.declarations[0];
292
+ return handle === undefined ? undefined : project.declarationAt(handle)?.node;
293
+ };
294
+ const assignedTypes = queries
295
+ .typesAt(assigned.map((one) => one.value))
296
+ .map((type, at) => partOf(must(type), assigned[at]?.path ?? []));
297
+ const byVariable = new Map();
298
+ assigned.forEach((_one, at) => {
299
+ const declaration = declarationOf(symbols[at]);
300
+ const type = assignedTypes[at];
301
+ if (declaration !== undefined && slots.has(declaration) && type !== undefined) {
302
+ byVariable.set(declaration, [...(byVariable.get(declaration) ?? []), type]);
303
+ }
304
+ });
305
+ return {
306
+ inside: (type) => inside.get(type.id) ?? [],
307
+ ofVariable: (node) => {
308
+ if (byVariable.size === 0 || !isIdentifier(node)) {
309
+ return undefined;
310
+ }
311
+ const [symbol] = queries.symbolsAt([node]).map(must);
312
+ const declaration = declarationOf(symbol);
313
+ return declaration === undefined ? undefined : byVariable.get(declaration);
314
+ },
315
+ outside,
316
+ };
317
+ }
@@ -10,6 +10,15 @@ import { byPosition, positionKey } from "../position.js";
10
10
  import { declarationKey } from "../ref.js";
11
11
  import { FAMILY_KEY_LENGTH } from "../resolve.js";
12
12
  import { isPartSubject, isRowSubject } from "../suppress.js";
13
+ /** The fixability grammar/analysis.md "Fixability" gives a finding about a held declaration. */
14
+ const MANUAL = "manual";
15
+ /** The member kinds an object property can declare, which an `in` expression tests by name. */
16
+ const IN_TESTED = new Set([
17
+ "class-member",
18
+ "method",
19
+ "type-member",
20
+ "interface-method",
21
+ ]);
13
22
  /** The classes ranked by the claim each makes, the strongest highest. */
14
23
  const RANK = { certain: 3, probable: 2, possible: 1 };
15
24
  /** The weaker of two classes, which is a class capped by a ceiling. */
@@ -38,18 +47,39 @@ function publishedIds(union) {
38
47
  .filter((root) => root.kind === "published-api")
39
48
  .flatMap((root) => union.symbols[root.at]?.id ?? []));
40
49
  }
50
+ /**
51
+ * The members whose name an `in` expression of a production file tests by a literal: a
52
+ * value narrowed that way is read back by the name, which no type links to the member.
53
+ */
54
+ function testedByIn(input) {
55
+ const names = input.files.inOperands ?? new Set();
56
+ const union = input.swept.matrix.union;
57
+ if (names.size === 0) {
58
+ return names;
59
+ }
60
+ return new Set(union.symbols.flatMap((symbol, at) => {
61
+ const parent = union.symbols[union.parent[at] ?? OUTSIDE];
62
+ return IN_TESTED.has(symbol.kind) &&
63
+ parent !== undefined &&
64
+ symbol.name.startsWith(`${parent.name}.`) &&
65
+ names.has(symbol.name.slice(parent.name.length + 1))
66
+ ? [symbol.id]
67
+ : [];
68
+ }));
69
+ }
41
70
  /**
42
71
  * The reachability class of one declaration of the run. A declaration of a library's
43
72
  * published API, a public member of a published type included, is `certain` where the
44
73
  * run loaded every consumer the scope declared and declared one, `probable` where one did
45
74
  * not load, and `possible` where it declared none, whatever the configuration says of the
46
- * consumer set. A declaration of test-support code that test code references, outside the
47
- * test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
75
+ * consumer set. Test-support code test code references, outside the test-of-dead-code
76
+ * rule, what it contains, and a member an `in` expression tests by name are `possible`.
48
77
  */
49
78
  function reachabilityClasses(input) {
79
+ const tested = testedByIn(input);
50
80
  const published = publishedIds(input.swept.matrix.union);
51
81
  const usedByTests = new Set(input.swept.sweep.candidates
52
- .filter((candidate) => candidate.heldByTests === candidate.id)
82
+ .filter((candidate) => candidate.heldByTests !== undefined)
53
83
  .map((candidate) => candidate.id));
54
84
  const { declared } = input.boundary.consumers;
55
85
  const outside = declared.length === 0
@@ -58,7 +88,7 @@ function reachabilityClasses(input) {
58
88
  ? "certain"
59
89
  : "probable";
60
90
  return (id) => {
61
- if (usedByTests.has(id)) {
91
+ if (usedByTests.has(id) || tested.has(id)) {
62
92
  return "possible";
63
93
  }
64
94
  return published.has(id) ? outside : "certain";
@@ -145,6 +175,7 @@ export function completed(input, findings, kinds = KINDS) {
145
175
  const classOf = reachabilityClasses(input);
146
176
  const ceilingOf = testCeilings(input, classOf);
147
177
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
178
+ const held = new Set(swept.sweep.components.filter((one) => one.held).flatMap((one) => one.members));
148
179
  let minted = swept.sweep.components.length;
149
180
  const dead = new Set(swept.sweep.components.map((component) => component.id));
150
181
  const fallingWith = new Map();
@@ -197,7 +228,14 @@ export function completed(input, findings, kinds = KINDS) {
197
228
  : { production: candidate.productionRefs, test: candidate.testRefs };
198
229
  const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
199
230
  const generated = input.files.generated?.has(finding.position.path) === true;
200
- const fixability = generated ? "none" : row.fixability;
231
+ const fixability = generated
232
+ ? "none"
233
+ : found !== undefined &&
234
+ symbol !== undefined &&
235
+ held.has(symbol.id) &&
236
+ row.findingFixabilities?.includes(MANUAL) === true
237
+ ? MANUAL
238
+ : row.fixability;
201
239
  const lastUses = fixability === "deletable" && candidate !== undefined
202
240
  ? input.dependencies.lastUses.get(candidate.id)
203
241
  : undefined;
@@ -259,24 +297,17 @@ function dialed(finding, minConfidence) {
259
297
  * below the minimum confidence is withheld, and so is every finding of the component of a
260
298
  * root the dials withhold, because a symbol that falls with a root is dead only through it
261
299
  * and its deletion does not compile while the root stays. A withheld finding that is no
262
- * root withholds itself alone. A code is withheld from a declaration when the finding it
263
- * would report there, completed, is withheld by its own severity or confidence.
300
+ * root withholds itself alone.
264
301
  */
265
302
  export function dialsOf(input, findings) {
266
303
  const minConfidence = input.config.analysis.minConfidence;
267
- const classOf = reachabilityClasses(input);
268
- const ceilingOf = testCeilings(input, classOf);
269
304
  const components = new Set(findings
270
305
  .filter((finding) => finding.component.root && dialed(finding, minConfidence))
271
306
  .map((finding) => finding.component.id));
272
307
  return {
273
- withholdsCode: (code, id) => {
308
+ allows: (code) => {
274
309
  const row = KINDS.get(code);
275
- return (row !== undefined &&
276
- dialed({
277
- severity: severityOf(input, code, row.defaultSeverity),
278
- confidence: capped(capped(classOf(id), row.maxClass), ceilingOf(id)),
279
- }, minConfidence));
310
+ return row !== undefined && severityOf(input, code, row.defaultSeverity) === "allow";
280
311
  },
281
312
  withholds: (finding) => dialed(finding, minConfidence) || components.has(finding.component.id),
282
313
  };
@@ -12,7 +12,7 @@ const MESSAGES = {
12
12
  */
13
13
  function unusedDependencies(dependencies) {
14
14
  return dependencies.declared
15
- .filter((dependency) => !dependencies.needed.has(dependency.name) &&
15
+ .filter((dependency) => !dependencies.needed.has(declarationKey(dependency)) &&
16
16
  !dependencies.ran.has(declarationKey(dependency)))
17
17
  .map((dependency) => ({
18
18
  code: UNUSED_DEPENDENCY,
@@ -64,7 +64,7 @@ function dialsOver(input, found, suppressed, unmarked) {
64
64
  const before = dialsOf(suppressed.unmarked, unmarked);
65
65
  const fromUnmarked = new Set(unmarked);
66
66
  return {
67
- withholdsCode: marked.withholdsCode,
67
+ allows: marked.allows,
68
68
  withholds: (finding) => fromUnmarked.has(finding) ? before.withholds(finding) : marked.withholds(finding),
69
69
  };
70
70
  }
@@ -1,6 +1,6 @@
1
1
  import { REPOSITORY_DOCUMENT } from "../config.js";
2
2
  import { FIXED_SEVERITY_CODES } from "../kinds.js";
3
- import { isRowCode, STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
3
+ import { isDependencyCode, isUnsuppressibleCode, STALE_SUPPRESSION, SUPPRESSION_WITHOUT_REASON, } from "../suppress.js";
4
4
  /** The code a configured root that matches nothing is reported under. */
5
5
  const UNMATCHED_ROOT = "DS1704";
6
6
  /** The code a configured declaration that names nothing is reported under. */
@@ -87,14 +87,27 @@ export function staleSuppressions(stale, files) {
87
87
  const reports = [...new Set(records.flatMap((one) => one.reports))]
88
88
  .filter((code) => !codes.includes(code))
89
89
  .sort();
90
- const rows = codes.filter(isRowCode);
90
+ const rows = codes.filter(isUnsuppressibleCode);
91
+ const dependencies = first.mechanism === "inline" ? codes.filter(isDependencyCode) : [];
91
92
  const settable = rows.filter((code) => !FIXED_SEVERITY_CODES.includes(code));
92
93
  const remedy = settable.length === 0
93
94
  ? "fix what the finding names instead"
94
95
  : `fix what the finding names or set the severity of ${spelled(settable)} instead`;
95
96
  const declared = reports.length === 0 ? "" : `its declaration reports ${spelled(reports)}`;
96
- const instead = rows.length > 0
97
- ? `, because ${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}${declared === "" ? "" : `; ${declared}`}`
97
+ const because = [
98
+ ...(rows.length > 0
99
+ ? [
100
+ `${spelled(rows)} ${rows.length === 1 ? "reports a row" : "report rows"} of a document, which no suppression can name; ${remedy}`,
101
+ ]
102
+ : []),
103
+ ...(dependencies.length > 0
104
+ ? [
105
+ `${spelled(dependencies)} ${dependencies.length === 1 ? "reports a dependency row" : "report dependency rows"}, which only an ignore entry or a baseline row can name`,
106
+ ]
107
+ : []),
108
+ ];
109
+ const instead = because.length > 0
110
+ ? `, because ${because.join("; ")}${declared === "" ? "" : `; ${declared}`}`
98
111
  : declared === ""
99
112
  ? ""
100
113
  : `, and ${declared}`;
@@ -6,7 +6,7 @@
6
6
  import { fails } from "./config.js";
7
7
  import { decidedFindings } from "./findings/emitters.js";
8
8
  import { fileRefsOf, staleSuppressions, } from "./findings/self-check.js";
9
- import { isRowSubject, pairs, suppressionCounts } from "./suppress.js";
9
+ import { isDependencySubject, isRowSubject, pairs, suppressionCounts, } from "./suppress.js";
10
10
  import { EXIT_CLEAN, EXIT_FINDINGS } from "./verbs/verb.js";
11
11
  /** The exit code of a report holding at least one pending finding. */
12
12
  export const EXIT_PENDING = 4;
@@ -78,14 +78,15 @@ export function verdictOf(result, config) {
78
78
  }
79
79
  /**
80
80
  * The rows one round of a baseline write records from its findings, in their order. A
81
- * finding about a row of a document is not recorded: no record withholds one, so a row
82
- * recording it would be stale on the first read back. Two findings that would record the
83
- * same row, two parts of one declaration under one code, record it once.
81
+ * finding about a row of a document other than a dependency row is not recorded: no record
82
+ * withholds one, so a row recording it would be stale on the first read back. Two findings
83
+ * that would record the same row, two parts of one declaration under one code, record it
84
+ * once.
84
85
  */
85
86
  export function recordedFindings(findings) {
86
87
  const rows = new Map();
87
88
  for (const finding of findings) {
88
- if (isRowSubject(finding.symbol.kind)) {
89
+ if (isRowSubject(finding.symbol.kind) && !isDependencySubject(finding.symbol.kind)) {
89
90
  continue;
90
91
  }
91
92
  const row = { code: finding.code, symbol: finding.symbol.ref, path: finding.position.path };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The property names `in` tests in a project's production files. A value narrowed by
3
+ * `"name" in value` is read back by that name, often through `unknown`, where no type
4
+ * links the read to a declaration, so only a text match can tell which member it reads.
5
+ */
6
+ import { isBinaryExpression, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
7
+ import { sourceFilesOf } from "./source-files.js";
8
+ /**
9
+ * The text of each string literal, or template literal with no substitution, written as
10
+ * the left operand of an `in` expression in a file of the project that is no test file.
11
+ * `testFiles` holds the test files by their path below the target root.
12
+ */
13
+ export function inOperandNames(project, targetRoot, testFiles) {
14
+ const tests = new Set(testFiles);
15
+ const found = new Set();
16
+ const visit = (node) => {
17
+ if (isBinaryExpression(node) &&
18
+ node.operatorToken.kind === SyntaxKind.InKeyword &&
19
+ (isStringLiteral(node.left) || isNoSubstitutionTemplateLiteral(node.left))) {
20
+ found.add(node.left.text);
21
+ }
22
+ node.forEachChild(visit);
23
+ };
24
+ for (const [path, file] of sourceFilesOf(project, targetRoot).byPath) {
25
+ if (!tests.has(path)) {
26
+ file.forEachChild(visit);
27
+ }
28
+ }
29
+ return [...found].sort();
30
+ }