@cplieger/deadset-ts 3.12.1 → 3.13.0

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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/src/analysis.js +100 -27
  3. package/dist/src/components.js +4 -2
  4. package/dist/src/configured-declarations.js +36 -0
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/edges.js +4 -1
  7. package/dist/src/findings/completion.js +37 -8
  8. package/dist/src/findings/emitters.js +5 -5
  9. package/dist/src/findings/intra-function.js +91 -2
  10. package/dist/src/findings/self-check.js +30 -7
  11. package/dist/src/findings/unused-declarations.js +15 -3
  12. package/dist/src/findings-pass.js +18 -11
  13. package/dist/src/intra-function-parts.js +643 -0
  14. package/dist/src/kinds.js +337 -35
  15. package/dist/src/reflective-lookup.js +35 -2
  16. package/dist/src/report.js +11 -2
  17. package/dist/src/reporters/sarif.js +20 -15
  18. package/dist/src/reporters/template.js +64 -5
  19. package/dist/src/serialization-contract.js +41 -22
  20. package/dist/src/signatures.js +32 -0
  21. package/dist/src/suppress-file.js +27 -6
  22. package/dist/src/suppress-inline.js +8 -1
  23. package/dist/src/suppress.js +27 -0
  24. package/dist/src/value-reach.js +33 -9
  25. package/dist/src/verbs/analyze.js +19 -11
  26. package/dist/src/verbs/describe.js +12 -8
  27. package/dist/src/version.js +1 -1
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +167 -48
  31. package/src/components.ts +4 -2
  32. package/src/configured-declarations.ts +57 -1
  33. package/src/conformance-record.ts +2 -2
  34. package/src/edges.ts +4 -1
  35. package/src/finding.ts +5 -1
  36. package/src/findings/completion.ts +46 -10
  37. package/src/findings/emitter.ts +3 -0
  38. package/src/findings/emitters.ts +9 -5
  39. package/src/findings/intra-function.ts +106 -3
  40. package/src/findings/self-check.ts +48 -7
  41. package/src/findings/unused-declarations.ts +18 -4
  42. package/src/findings-pass.ts +26 -11
  43. package/src/intra-function-parts.ts +871 -0
  44. package/src/kinds.ts +355 -41
  45. package/src/reflective-lookup.ts +42 -2
  46. package/src/report.ts +18 -2
  47. package/src/reporters/sarif.ts +24 -16
  48. package/src/reporters/template.ts +69 -6
  49. package/src/serialization-contract.ts +47 -30
  50. package/src/signatures.ts +55 -0
  51. package/src/suppress-file.ts +34 -6
  52. package/src/suppress-inline.ts +8 -1
  53. package/src/suppress.ts +31 -0
  54. package/src/value-reach.ts +53 -12
  55. package/src/verbs/analyze.ts +26 -11
  56. package/src/verbs/describe.ts +13 -9
  57. package/src/version.ts +1 -1
  58. package/dist/src/reporters/rules.js +0 -178
  59. package/src/reporters/rules.ts +0 -195
@@ -1,9 +1,8 @@
1
1
  import type { Confidence, Severity } from "../config.ts";
2
- import { KINDS } from "../kinds.ts";
2
+ import { KINDS, type KindRow } from "../kinds.ts";
3
3
  import type { Report, WireFinding, WireStaleSuppression } from "../report.ts";
4
4
  import { lineHashes, symbolFingerprint } from "./fingerprint.ts";
5
5
  import { RenderError, type RenderOptions } from "./reporter.ts";
6
- import { RULE_TEXTS } from "./rules.ts";
7
6
  import { utf8Bytes } from "./sha256.ts";
8
7
 
9
8
  const SCHEMA =
@@ -12,7 +11,7 @@ const VERSION = "2.1.0";
12
11
  /** The unit a finding's column counts. */
13
12
  const COLUMN_KIND = "utf16CodeUnits";
14
13
  const URI_BASE_ID = "%SRCROOT%";
15
- const ROOT_DESCRIPTION = "The target root, the directory the analyzer was run on.";
14
+ const ROOT_DESCRIPTION = "The target root.";
16
15
 
17
16
  /** The most related locations one result carries; the report stays complete beyond them. */
18
17
  const MAX_RELATED = 100;
@@ -41,23 +40,26 @@ function firstSentence(text: string): string {
41
40
  return text;
42
41
  }
43
42
 
43
+ /** Every live kind whose languages include one of the run's, in code order. */
44
+ function kindsOf(languages: readonly string[]): readonly KindRow[] {
45
+ return [...KINDS.values()].filter((kind) =>
46
+ kind.languages.some((language) => languages.includes(language)),
47
+ );
48
+ }
49
+
44
50
  /**
45
- * One rule per live kind of this analyzer's language, in code order, whether or not a result
51
+ * One rule per live kind of the run's languages, in code order, whether or not a result
46
52
  * names it, so a rule's index is the same in every run and under every configuration.
47
53
  */
48
- function rules(): readonly Record<string, unknown>[] {
49
- return [...RULE_TEXTS].map(([code, text]) => {
50
- const kind = KINDS.get(code);
51
- if (kind === undefined) {
52
- throw new RenderError(`the rule ${code} names no live kind`);
53
- }
54
+ function rules(kinds: readonly KindRow[]): readonly Record<string, unknown>[] {
55
+ return kinds.map((kind) => {
54
56
  return {
55
- id: code,
57
+ id: kind.code,
56
58
  name: kind.name,
57
- shortDescription: { text: firstSentence(text.rule) },
58
- fullDescription: { text: text.rule },
59
+ shortDescription: { text: firstSentence(kind.rule) },
60
+ fullDescription: { text: kind.rule },
59
61
  help: {
60
- text: text.precondition === undefined ? text.rule : `${text.rule}\n\n${text.precondition}`,
62
+ text: kind.precondition === undefined ? kind.rule : `${kind.rule}\n\n${kind.precondition}`,
61
63
  },
62
64
  defaultConfiguration: { level: LEVEL[kind.defaultSeverity] },
63
65
  properties: {
@@ -155,6 +157,11 @@ function relatedOf(found: WireFinding): readonly Related[] {
155
157
  for (const at of found.details.write_positions ?? []) {
156
158
  related.push({ label: "write", at, endLine: at.end_line });
157
159
  }
160
+ for (const one of found.component.members ?? []) {
161
+ if (one.ref !== found.symbol.ref) {
162
+ related.push({ label: "member", at: one.position, endLine: one.position.end_line });
163
+ }
164
+ }
158
165
  return related.slice(0, MAX_RELATED);
159
166
  }
160
167
 
@@ -254,8 +261,9 @@ function staleResult(
254
261
  * rendering, because a line fingerprint computed from other bytes opens a second alert.
255
262
  */
256
263
  export function sarif(report: Report, options: RenderOptions): string {
257
- const ruleList = rules();
258
- const index = new Map([...RULE_TEXTS.keys()].map((code, at) => [code, at]));
264
+ const kinds = kindsOf(report.analyzer.languages);
265
+ const ruleList = rules(kinds);
266
+ const index = new Map(kinds.map((kind, at) => [kind.code, at]));
259
267
  const hashOf = lineHashCache(options.readSource);
260
268
  const results = [
261
269
  ...report.findings.map((found) => findingResult(found, index, hashOf)),
@@ -115,7 +115,11 @@ const ESCAPES: Readonly<Record<string, string>> = {
115
115
  const ESCAPE_WIDTHS: Readonly<Record<string, number>> = { x: 2, u: 4, U: 8 };
116
116
 
117
117
  const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
118
- const NUMBER = /[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;
118
+ /** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
119
+ const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
120
+
121
+ /** The one number form the subset admits: a decimal integer with no leading zero. */
122
+ const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
119
123
 
120
124
  function lineOf(source: string, at: number): number {
121
125
  let line = 1;
@@ -224,6 +228,11 @@ function lexAction(
224
228
  NUMBER.lastIndex = i;
225
229
  const number = NUMBER.exec(source)?.[0];
226
230
  if (number !== undefined) {
231
+ if (!DECIMAL_INTEGER.test(number)) {
232
+ throw new TemplateError(
233
+ `line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`,
234
+ );
235
+ }
227
236
  tokens.push({ t: "lit", value: Number(number) });
228
237
  i += number.length;
229
238
  continue;
@@ -636,7 +645,61 @@ function compareBasic(name: string, a: Value, b: Value): number {
636
645
  if (kind === "boolean") {
637
646
  return Number.NaN;
638
647
  }
639
- return (a as number | string) < (b as number | string) ? -1 : 1;
648
+ if (kind === "string") {
649
+ return byCodePoints(a as string, b as string);
650
+ }
651
+ return (a as number) < (b as number) ? -1 : 1;
652
+ }
653
+
654
+ /**
655
+ * Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
656
+ * code points: the order of their UTF-16 code units differs above U+FFFF.
657
+ */
658
+ function byCodePoints(a: string, b: string): number {
659
+ let at = 0;
660
+ while (at < a.length && at < b.length) {
661
+ const x = a.codePointAt(at) ?? 0;
662
+ const y = b.codePointAt(at) ?? 0;
663
+ if (x !== y) {
664
+ return x < y ? -1 : 1;
665
+ }
666
+ at += x > 0xffff ? 2 : 1;
667
+ }
668
+ return a.length - b.length;
669
+ }
670
+
671
+ /** The escapes `%q` writes for the control characters that have one of their own. */
672
+ const QUOTED_ESCAPES: Readonly<Record<string, string>> = {
673
+ '"': '\\"',
674
+ "\\": "\\\\",
675
+ "\u0007": "\\a",
676
+ "\b": "\\b",
677
+ "\f": "\\f",
678
+ "\n": "\\n",
679
+ "\r": "\\r",
680
+ "\t": "\\t",
681
+ "\v": "\\v",
682
+ };
683
+
684
+ /**
685
+ * The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
686
+ * each control character with an escape of its own written as it, every other character
687
+ * below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
688
+ * other character as itself.
689
+ */
690
+ function goQuoted(text: string): string {
691
+ let out = '"';
692
+ for (const char of text) {
693
+ const code = char.codePointAt(0) ?? 0;
694
+ if (Object.hasOwn(QUOTED_ESCAPES, char)) {
695
+ out += QUOTED_ESCAPES[char] ?? "";
696
+ } else if (code < 0x20 || code === 0x7f) {
697
+ out += `\\x${code.toString(16).padStart(2, "0")}`;
698
+ } else {
699
+ out += char;
700
+ }
701
+ }
702
+ return `${out}"`;
640
703
  }
641
704
 
642
705
  /** `printf`'s verbs, each over one operand. */
@@ -663,10 +726,10 @@ function printf(format: string, args: readonly Value[]): string {
663
726
  switch (verb) {
664
727
  case "v":
665
728
  case "s":
666
- out += formatted(arg, false);
729
+ out += formatted(arg, true);
667
730
  break;
668
731
  case "q":
669
- out += JSON.stringify(formatted(arg, false));
732
+ out += goQuoted(formatted(arg, true));
670
733
  break;
671
734
  case "d":
672
735
  if (typeof arg !== "number" || !Number.isInteger(arg)) {
@@ -768,11 +831,11 @@ function call(name: string, args: readonly Value[]): Value {
768
831
  return args
769
832
  .map((arg, i) => {
770
833
  const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
771
- return `${spaced ? " " : ""}${formatted(arg, false)}`;
834
+ return `${spaced ? " " : ""}${formatted(arg, true)}`;
772
835
  })
773
836
  .join("");
774
837
  case "println":
775
- return `${args.map((arg) => formatted(arg, false)).join(" ")}\n`;
838
+ return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
776
839
  case "printf":
777
840
  if (typeof first !== "string") {
778
841
  throw new RenderError("printf takes a format string first");
@@ -1,11 +1,12 @@
1
1
  /**
2
- * The serialization-contract class: the properties of a class whose values reach
3
- * `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
4
- * `toString` where a value is passed to an `unknown` or `any` parameter of a function
5
- * outside the analysis. A function of the project handing such a parameter of its own
6
- * to a destination is one, holding back what that destination does, to a fixpoint. A
7
- * function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
8
- * given the arguments those methods pass it.
2
+ * The serialization-contract class: the data members and `toJSON` of every class,
3
+ * interface and object type a value's static type names, and of every such type its
4
+ * data members' types reach, where the value reaches `JSON.stringify` or a call
5
+ * `ts.serializers` names, and those and `toString` where it is passed to an `unknown`
6
+ * or `any` parameter of a function outside the analysis. A function of the project
7
+ * handing such a parameter of its own to a destination is one, holding back what that
8
+ * destination does, to a fixpoint. A function invoked through `Function.prototype.call`,
9
+ * `apply` or `bind` is the callee, given the arguments those methods pass it.
9
10
  */
10
11
 
11
12
  import {
@@ -57,7 +58,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
57
58
  import { nodeKey, type Inventory } from "./inventory.ts";
58
59
  import { renderPosition } from "./position.ts";
59
60
  import type { ProjectView } from "./session.ts";
60
- import { valueReach } from "./value-reach.ts";
61
+ import { valueReach, type Reached } from "./value-reach.ts";
61
62
 
62
63
  /** The serializer every run reads, whatever the configuration names. */
63
64
  const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
@@ -65,6 +66,9 @@ const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.string
65
66
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
66
67
  const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
67
68
 
69
+ /** The method a serializer calls on a value that carries one, wherever the value goes. */
70
+ const SERIALIZED_CONVERSION = "toJSON";
71
+
68
72
  /** The parameter types that keep nothing of the value they are given. */
69
73
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
70
74
 
@@ -521,10 +525,12 @@ function destinations<Brand>(
521
525
  return { invocation, at, mayReceive };
522
526
  }
523
527
 
524
- /** One argument whose value carries classes of the target into a destination. */
528
+ /** One argument whose value carries classes and object types of the target into a destination. */
525
529
  interface Flow {
526
530
  readonly site: Node;
527
531
  readonly classes: ReadonlySet<string>;
532
+ /** The members of the interfaces and object types the value carries. */
533
+ readonly members: readonly TSSymbol[];
528
534
  readonly to: Destination;
529
535
  }
530
536
 
@@ -554,31 +560,34 @@ function flowsOf<Brand>(
554
560
  );
555
561
  return arguments_.flatMap((one) => {
556
562
  const type = typeOf.get(one.value);
557
- let classes: ReadonlySet<string> = new Set<string>();
563
+ let reached: Reached = { classes: new Set<string>(), members: [] };
558
564
  if (one.value.kind === SyntaxKind.ThisKeyword) {
559
- classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
565
+ reached = {
566
+ classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
567
+ members: [],
568
+ };
560
569
  } else if (type !== undefined) {
561
- classes = reach.classesOf(type);
570
+ reached = reach.reachedFrom(type);
562
571
  }
563
- const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
564
- return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
572
+ const carries = reached.classes.size > 0 || reached.members.length > 0;
573
+ const destination = carries ? to.at(one.site.call, one.argument) : undefined;
574
+ return destination === undefined
575
+ ? []
576
+ : [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
565
577
  });
566
578
  }
567
579
 
568
580
  /**
569
- * The serialization-contract detector: each property of each class whose values reach
570
- * a destination, and the two conversion methods where the destination is outside the
571
- * analysis, recorded at the argument with the callee named.
581
+ * The serialization-contract detector: each data member and `toJSON` of each class,
582
+ * interface and object type a value that reaches a destination carries, and `toString`
583
+ * as well where the destination is outside the analysis, recorded at the argument with
584
+ * the callee named.
572
585
  */
573
586
  export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
574
587
  const { project, held, targetRoot } = input;
575
588
  const walked = walkProject(project, held);
576
- const flows = flowsOf(
577
- project,
578
- held,
579
- walked.sites,
580
- destinations(input, aliasChains(project, held), walked),
581
- );
589
+ const chains = aliasChains(project, held);
590
+ const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
582
591
  if (flows.length === 0) {
583
592
  return [];
584
593
  }
@@ -589,22 +598,30 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
589
598
  "instance",
590
599
  );
591
600
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
601
+ // A destination holds back a data member and `toJSON`, and `toString` as well where
602
+ // the value left the analysis.
603
+ const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
604
+ (flags & SymbolFlags.Property) !== 0 ||
605
+ ((flags & SymbolFlags.Method) !== 0 &&
606
+ (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
592
607
  const found: Evidence[] = [];
593
608
  for (const flow of flows) {
594
609
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
595
610
  const detail = `passed to ${flow.to.callee}`;
596
611
  for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
597
612
  const symbol = byId.get(member.id);
598
- const isProperty = (member.flags & SymbolFlags.Property) !== 0;
599
- const converts =
600
- flow.to.reach === "conversions" &&
601
- (member.flags & SymbolFlags.Method) !== 0 &&
602
- symbol !== undefined &&
603
- CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
604
- if (isProperty || converts) {
613
+ const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
614
+ if (holdsBack(member.flags, name, flow.to)) {
605
615
  found.push({ id: member.id, detail, site });
606
616
  }
607
617
  }
618
+ for (const member of flow.members) {
619
+ if (holdsBack(member.flags, member.name, flow.to)) {
620
+ for (const id of chains.declarationsOf(member)) {
621
+ found.push({ id, detail, site });
622
+ }
623
+ }
624
+ }
608
625
  }
609
626
  return found;
610
627
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The function a declaration of the inventory declares, where it declares one: the node
3
+ * whose parameters, body and result are the declaration's own parts.
4
+ */
5
+
6
+ import {
7
+ isArrowFunction,
8
+ isClassDeclaration,
9
+ isClassExpression,
10
+ isFunctionDeclaration,
11
+ isFunctionExpression,
12
+ isMethodDeclaration,
13
+ isPropertyDeclaration,
14
+ isVariableDeclaration,
15
+ type ArrowFunction,
16
+ type FunctionDeclaration,
17
+ type FunctionExpression,
18
+ type MethodDeclaration,
19
+ type Node,
20
+ } from "@typescript/native/unstable/ast";
21
+
22
+ /** A function-like node that carries a body of its own. */
23
+ export type SignatureNode =
24
+ FunctionDeclaration | MethodDeclaration | ArrowFunction | FunctionExpression;
25
+
26
+ /** Whether one node is a function value written in place: an arrow or a function expression. */
27
+ function isFunctionValue(node: Node | undefined): node is ArrowFunction | FunctionExpression {
28
+ return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
29
+ }
30
+
31
+ /**
32
+ * The function one declaration declares: a function declaration and a class method with
33
+ * a body are their own; a variable and a class property whose initializer is an arrow or
34
+ * a function expression declare that function. Any other declaration declares none, an
35
+ * overload signature and an abstract or ambient method among them, because no body is
36
+ * there to read its parts.
37
+ */
38
+ export function signatureOf(node: Node): SignatureNode | undefined {
39
+ if (isFunctionDeclaration(node)) {
40
+ return node.body === undefined ? undefined : node;
41
+ }
42
+ if (isMethodDeclaration(node)) {
43
+ const owner = node.parent;
44
+ return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
45
+ ? node
46
+ : undefined;
47
+ }
48
+ if (
49
+ (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
50
+ isFunctionValue(node.initializer)
51
+ ) {
52
+ return node.initializer;
53
+ }
54
+ return undefined;
55
+ }
@@ -154,6 +154,15 @@ function readRecord(value: unknown, site: Position, byReference: ByReference, sh
154
154
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
155
155
  }
156
156
 
157
+ function byReferenceOf(symbols: readonly InventorySymbol[]): ByReference {
158
+ const byReference = new Map<string, InventorySymbol[]>();
159
+ for (const symbol of symbols) {
160
+ const key = referenceKey(symbol.ref, symbol.position.path);
161
+ byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
162
+ }
163
+ return byReference;
164
+ }
165
+
157
166
  /** Reads one document at the target root, binding each record against the run's declarations. */
158
167
  function readDocument(
159
168
  host: Host,
@@ -196,11 +205,7 @@ function readDocument(
196
205
  return malformed(shape.file);
197
206
  }
198
207
 
199
- const byReference = new Map<string, InventorySymbol[]>();
200
- for (const symbol of symbols) {
201
- const key = referenceKey(symbol.ref, symbol.position.path);
202
- byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
203
- }
208
+ const byReference = byReferenceOf(symbols);
204
209
  const records: SuppressionRecord[] = [];
205
210
  const refusals: Refusal[] = [];
206
211
  (held as readonly unknown[]).forEach((value, index) => {
@@ -247,6 +252,24 @@ export interface Recorded {
247
252
  readonly path: string;
248
253
  }
249
254
 
255
+ /**
256
+ * Rows a baseline write holds in memory, bound against the run's declarations as the same
257
+ * rows read back from the document would be, each carrying the reason given.
258
+ */
259
+ export function bindRows(
260
+ rows: readonly Recorded[],
261
+ reason: string,
262
+ symbols: readonly InventorySymbol[],
263
+ ): Suppressions {
264
+ const byReference = byReferenceOf(symbols);
265
+ const site = documentSite(BASELINE_SHAPE);
266
+ const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
267
+ return {
268
+ records: read.flatMap((one) => one.records),
269
+ refusals: read.flatMap((one) => one.refusals),
270
+ };
271
+ }
272
+
250
273
  /** The analyzer a written row names as its provenance, as the same run's report names it. */
251
274
  export interface Provenance {
252
275
  readonly analyzer: string;
@@ -258,6 +281,11 @@ const BASELINE_DESCRIPTION =
258
281
  "Recorded findings: a run fails only on a finding this document does not hold. " +
259
282
  "Every reason is the analyzer that recorded the row, never an adjudication.";
260
283
 
284
+ /** The reason every row a baseline write records carries. */
285
+ export function rowReason(provenance: Provenance): string {
286
+ return `recorded by ${provenance.analyzer} ${provenance.version}`;
287
+ }
288
+
261
289
  /**
262
290
  * A baseline holding one row per finding, in the order given, which is the report's. Every
263
291
  * row carries the provenance as its reason. A finding missing a value a row needs, and an
@@ -270,7 +298,7 @@ export function writeBaseline(findings: readonly Recorded[], provenance: Provena
270
298
  `a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`,
271
299
  );
272
300
  }
273
- const reason = `recorded by ${provenance.analyzer} ${provenance.version}`;
301
+ const reason = rowReason(provenance);
274
302
  const baseline = findings.map((found) => {
275
303
  if (found.code === "" || found.symbol === "" || found.path === "") {
276
304
  throw new Error(
@@ -8,6 +8,7 @@ import { SyntaxKind, type Node, type SourceFile } from "@typescript/native/unsta
8
8
  import { nodeKey, type Inventory } from "./inventory.ts";
9
9
  import { byPosition, positionKey, renderPosition, type Position } from "./position.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
+ import { signatureOf } from "./signatures.ts";
11
12
  import {
12
13
  SUPPRESSION_WITHOUT_REASON,
13
14
  SuppressionError,
@@ -168,7 +169,8 @@ interface Declared {
168
169
  /**
169
170
  * The declarations of one file by the line each begins on, the line of its first token: a
170
171
  * decorator, a modifier or the keyword. A variable's first token is its statement's
171
- * keyword, and a variable written on a later line of the statement also begins there.
172
+ * keyword, and a variable written on a later line of the statement also begins there. A
173
+ * function also begins, for a directive, on the line of each of its parameters.
172
174
  */
173
175
  function declarationLines(
174
176
  file: SourceFile,
@@ -189,6 +191,11 @@ function declarationLines(
189
191
  const one = id === undefined ? undefined : symbols.get(id);
190
192
  if (one !== undefined) {
191
193
  add(node.pos, one);
194
+ // A parameter on a later line of a wrapped signature takes its directive on the
195
+ // line above it, inside the parameter list, and the directive binds the function.
196
+ for (const parameter of signatureOf(node)?.parameters ?? []) {
197
+ add(parameter.pos, one);
198
+ }
192
199
  const list = node.parent;
193
200
  if (
194
201
  node.kind === SyntaxKind.VariableDeclaration &&
package/src/suppress.ts CHANGED
@@ -169,6 +169,7 @@ const ROW_SUBJECTS: ReadonlySet<string> = new Set([
169
169
  "dependency",
170
170
  "module-directive",
171
171
  "root",
172
+ "configured-declaration",
172
173
  "suppression",
173
174
  "edge",
174
175
  ]);
@@ -178,6 +179,36 @@ export function isRowSubject(kind: string): boolean {
178
179
  return ROW_SUBJECTS.has(kind);
179
180
  }
180
181
 
182
+ /**
183
+ * The subject kinds that are a part of a declaration rather than a declaration: a part
184
+ * has no reference of its own, so a finding about one names the declaration that holds
185
+ * it, and the part is decided inside that declaration whatever uses it.
186
+ */
187
+ const PART_SUBJECTS: ReadonlySet<string> = new Set([
188
+ "parameter",
189
+ "receiver",
190
+ "result",
191
+ "statement",
192
+ "case",
193
+ "store",
194
+ ]);
195
+
196
+ /** Whether a finding's subject is a part of the declaration its reference names. */
197
+ export function isPartSubject(kind: string): boolean {
198
+ return PART_SUBJECTS.has(kind);
199
+ }
200
+
201
+ /** The prefix of the codes of the part kinds, the intra-function range. */
202
+ const PART_KIND_PREFIX = "DS18";
203
+
204
+ /**
205
+ * Whether a code is a part kind, whose record withholds its finding and marks nothing:
206
+ * the record says the part is wanted, not that the declaration holding it is.
207
+ */
208
+ export function isPartKind(code: string): boolean {
209
+ return code.startsWith(PART_KIND_PREFIX);
210
+ }
211
+
181
212
  /** One finding's identity: its code and the position of the thing it names. */
182
213
  function claimKey(finding: Finding): string {
183
214
  return `${finding.code}\u0000${positionKey(finding.position)}`;
@@ -1,9 +1,11 @@
1
1
  /**
2
- * The classes of the target a value of one type carries, as a serializer walking the
3
- * value reaches them: through union and intersection constituents, array and tuple
4
- * elements, and the data members of the target's classes and of object types the
5
- * target writes. The walk stops at `any`, `unknown`, a type parameter, a function and
6
- * an interface or class of another program, so an instance held only there is missed.
2
+ * The classes of the target a value of one type carries, and the members of the other
3
+ * object types the target writes that it carries, as a serializer walking the value
4
+ * reaches them: through union and intersection constituents, array and tuple elements,
5
+ * and the data members and index signatures of the target's classes and of the
6
+ * interfaces and object types the target writes. The walk stops at `any`, `unknown`, a
7
+ * type parameter, a function and an interface or class of another program, so an
8
+ * instance held only there is missed.
7
9
  */
8
10
 
9
11
  import {
@@ -23,12 +25,25 @@ const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
23
25
  /** The symbols of an anonymous object type the target writes as a type. */
24
26
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
25
27
 
26
- /** The classes a type's values carry, each asked of the checker once. */
28
+ /** What a value of one type carries that the target declares. */
29
+ export interface Reached {
30
+ /** The identifiers of the target's classes. */
31
+ readonly classes: ReadonlySet<string>;
32
+ /**
33
+ * The members of the interfaces and object types the target writes, other than its
34
+ * classes: each data member, and each method named `toJSON` or `toString`.
35
+ */
36
+ readonly members: readonly TSSymbol[];
37
+ }
38
+
39
+ /** What each type's values carry, each type asked of the checker once. */
27
40
  export interface ValueReach {
28
- /** The identifiers of the target's classes a value of `type` carries. */
29
- classesOf(type: Type): ReadonlySet<string>;
41
+ reachedFrom(type: Type): Reached;
30
42
  }
31
43
 
44
+ /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
45
+ const CONVERSION_NAMES: ReadonlySet<string> = new Set(["toJSON", "toString"]);
46
+
32
47
  /** The global array types, whose element type is what an array value carries. */
33
48
  function arrayTargets<Brand>(project: ProjectView<Brand>): ReadonlySet<number> {
34
49
  const checker = project.checker;
@@ -105,6 +120,29 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
105
120
  ];
106
121
  };
107
122
 
123
+ /** Whether the walk reads one type's members because the target writes the type, not as a class. */
124
+ const writtenObject = (type: Type): boolean => {
125
+ if (!type.isObjectType() || classOf(type) !== undefined) {
126
+ return false;
127
+ }
128
+ const target = targetOf(type);
129
+ const isList =
130
+ type.isTypeReference() &&
131
+ (arrays.has(target.id) ||
132
+ (target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
133
+ return !isList && isWritten(type);
134
+ };
135
+
136
+ /** The data members and conversion methods of one written object type. */
137
+ const carried = (type: Type): readonly TSSymbol[] =>
138
+ checker
139
+ .getPropertiesOfType(type)
140
+ .filter(
141
+ (property) =>
142
+ (property.flags & SymbolFlags.Property) !== 0 ||
143
+ ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)),
144
+ );
145
+
108
146
  /** The types a value of one type holds, read once per type. */
109
147
  const childrenOf = (type: Type): readonly Type[] => {
110
148
  const known = children.get(type.id);
@@ -131,8 +169,9 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
131
169
  };
132
170
 
133
171
  return {
134
- classesOf: (type) => {
135
- const found = new Set<string>();
172
+ reachedFrom: (type) => {
173
+ const classes = new Set<string>();
174
+ const members: TSSymbol[] = [];
136
175
  const seen = new Set<number>();
137
176
  const pending: Type[] = [type];
138
177
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
@@ -142,11 +181,13 @@ export function valueReach<Brand>(project: ProjectView<Brand>, held: Inventory):
142
181
  seen.add(next.id);
143
182
  const owner = classOf(next);
144
183
  if (owner !== undefined) {
145
- found.add(owner);
184
+ classes.add(owner);
185
+ } else if (writtenObject(next)) {
186
+ members.push(...carried(next));
146
187
  }
147
188
  pending.push(...childrenOf(next));
148
189
  }
149
- return found;
190
+ return { classes, members };
150
191
  },
151
192
  };
152
193
  }