@cplieger/deadset-ts 3.12.0 → 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 (63) 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/graph.js +7 -7
  14. package/dist/src/intra-function-parts.js +643 -0
  15. package/dist/src/kinds.js +337 -35
  16. package/dist/src/reflective-lookup.js +35 -2
  17. package/dist/src/report.js +11 -2
  18. package/dist/src/reporters/sarif.js +20 -15
  19. package/dist/src/reporters/template.js +64 -5
  20. package/dist/src/roots.js +13 -8
  21. package/dist/src/serialization-contract.js +41 -22
  22. package/dist/src/signatures.js +32 -0
  23. package/dist/src/suppress-file.js +27 -6
  24. package/dist/src/suppress-inline.js +8 -1
  25. package/dist/src/suppress.js +27 -0
  26. package/dist/src/value-reach.js +33 -9
  27. package/dist/src/verbs/analyze.js +19 -11
  28. package/dist/src/verbs/describe.js +12 -8
  29. package/dist/src/version.js +1 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/analysis.ts +167 -48
  33. package/src/components.ts +4 -2
  34. package/src/configured-declarations.ts +57 -1
  35. package/src/conformance-record.ts +2 -2
  36. package/src/edges.ts +4 -1
  37. package/src/finding.ts +5 -1
  38. package/src/findings/completion.ts +46 -10
  39. package/src/findings/emitter.ts +3 -0
  40. package/src/findings/emitters.ts +9 -5
  41. package/src/findings/intra-function.ts +106 -3
  42. package/src/findings/self-check.ts +48 -7
  43. package/src/findings/unused-declarations.ts +18 -4
  44. package/src/findings-pass.ts +26 -11
  45. package/src/graph.ts +7 -7
  46. package/src/intra-function-parts.ts +871 -0
  47. package/src/kinds.ts +355 -41
  48. package/src/reflective-lookup.ts +42 -2
  49. package/src/report.ts +18 -2
  50. package/src/reporters/sarif.ts +24 -16
  51. package/src/reporters/template.ts +69 -6
  52. package/src/roots.ts +14 -9
  53. package/src/serialization-contract.ts +47 -30
  54. package/src/signatures.ts +55 -0
  55. package/src/suppress-file.ts +34 -6
  56. package/src/suppress-inline.ts +8 -1
  57. package/src/suppress.ts +31 -0
  58. package/src/value-reach.ts +53 -12
  59. package/src/verbs/analyze.ts +26 -11
  60. package/src/verbs/describe.ts +13 -9
  61. package/src/version.ts +1 -1
  62. package/dist/src/reporters/rules.js +0 -178
  63. package/src/reporters/rules.ts +0 -195
@@ -49,7 +49,10 @@ const ESCAPES = {
49
49
  /** The number of hexadecimal digits each numeric escape takes. */
50
50
  const ESCAPE_WIDTHS = { x: 2, u: 4, U: 8 };
51
51
  const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/uy;
52
- const NUMBER = /[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/uy;
52
+ /** A number as the lexer reads one, before the subset decides whether it is a decimal integer. */
53
+ const NUMBER = /[+-]?[0-9][0-9A-Za-z_.]*/uy;
54
+ /** The one number form the subset admits: a decimal integer with no leading zero. */
55
+ const DECIMAL_INTEGER = /^[+-]?(?:0|[1-9][0-9]*)$/u;
53
56
  function lineOf(source, at) {
54
57
  let line = 1;
55
58
  for (let i = 0; i < at; i += 1) {
@@ -153,6 +156,9 @@ function lexAction(source, at, line) {
153
156
  NUMBER.lastIndex = i;
154
157
  const number = NUMBER.exec(source)?.[0];
155
158
  if (number !== undefined) {
159
+ if (!DECIMAL_INTEGER.test(number)) {
160
+ throw new TemplateError(`line ${String(line)}: ${JSON.stringify(number)} is not a decimal integer`);
161
+ }
156
162
  tokens.push({ t: "lit", value: Number(number) });
157
163
  i += number.length;
158
164
  continue;
@@ -530,8 +536,61 @@ function compareBasic(name, a, b) {
530
536
  if (kind === "boolean") {
531
537
  return Number.NaN;
532
538
  }
539
+ if (kind === "string") {
540
+ return byCodePoints(a, b);
541
+ }
533
542
  return a < b ? -1 : 1;
534
543
  }
544
+ /**
545
+ * Two strings ordered by the bytes of their UTF-8 encodings, which is the order of their
546
+ * code points: the order of their UTF-16 code units differs above U+FFFF.
547
+ */
548
+ function byCodePoints(a, b) {
549
+ let at = 0;
550
+ while (at < a.length && at < b.length) {
551
+ const x = a.codePointAt(at) ?? 0;
552
+ const y = b.codePointAt(at) ?? 0;
553
+ if (x !== y) {
554
+ return x < y ? -1 : 1;
555
+ }
556
+ at += x > 0xffff ? 2 : 1;
557
+ }
558
+ return a.length - b.length;
559
+ }
560
+ /** The escapes `%q` writes for the control characters that have one of their own. */
561
+ const QUOTED_ESCAPES = {
562
+ '"': '\\"',
563
+ "\\": "\\\\",
564
+ "\u0007": "\\a",
565
+ "\b": "\\b",
566
+ "\f": "\\f",
567
+ "\n": "\\n",
568
+ "\r": "\\r",
569
+ "\t": "\\t",
570
+ "\v": "\\v",
571
+ };
572
+ /**
573
+ * The text `%q` writes: double quotes around the text, a quote and a backslash escaped,
574
+ * each control character with an escape of its own written as it, every other character
575
+ * below U+0020 and U+007F written as `\x` and two lowercase hexadecimal digits, and every
576
+ * other character as itself.
577
+ */
578
+ function goQuoted(text) {
579
+ let out = '"';
580
+ for (const char of text) {
581
+ const code = char.codePointAt(0) ?? 0;
582
+ if (Object.hasOwn(QUOTED_ESCAPES, char)) {
583
+ out += QUOTED_ESCAPES[char] ?? "";
584
+ }
585
+ else if (code < 0x20 || code === 0x7f) {
586
+ out += `\\x${code.toString(16).padStart(2, "0")}`;
587
+ }
588
+ else {
589
+ out += char;
590
+ }
591
+ }
592
+ return `${out}"`;
593
+ }
535
594
  /** `printf`'s verbs, each over one operand. */
536
595
  function printf(format, args) {
537
596
  let out = "";
@@ -556,10 +615,10 @@ function printf(format, args) {
556
615
  switch (verb) {
557
616
  case "v":
558
617
  case "s":
559
- out += formatted(arg, false);
618
+ out += formatted(arg, true);
560
619
  break;
561
620
  case "q":
562
- out += JSON.stringify(formatted(arg, false));
621
+ out += goQuoted(formatted(arg, true));
563
622
  break;
564
623
  case "d":
565
624
  if (typeof arg !== "number" || !Number.isInteger(arg)) {
@@ -659,11 +718,11 @@ function call(name, args) {
659
718
  return args
660
719
  .map((arg, i) => {
661
720
  const spaced = i > 0 && typeof arg !== "string" && typeof args[i - 1] !== "string";
662
- return `${spaced ? " " : ""}${formatted(arg, false)}`;
721
+ return `${spaced ? " " : ""}${formatted(arg, true)}`;
663
722
  })
664
723
  .join("");
665
724
  case "println":
666
- return `${args.map((arg) => formatted(arg, false)).join(" ")}\n`;
725
+ return `${args.map((arg) => formatted(arg, true)).join(" ")}\n`;
667
726
  case "printf":
668
727
  if (typeof first !== "string") {
669
728
  throw new RenderError("printf takes a format string first");
package/dist/src/roots.js CHANGED
@@ -4,9 +4,8 @@
4
4
  *
5
5
  * Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
6
6
  * that names a caller is live under reference counting as well, which the sweep
7
- * decides; a root that only supposes a consumer is not, so an exported declaration
8
- * something outside the program reaches is live under reachability and still a
9
- * candidate under reference counting.
7
+ * decides; the published API only supposes a consumer, so a published export is live
8
+ * under reachability and still a candidate under reference counting.
10
9
  */
11
10
  import { aliasChains } from "./alias-chain.js";
12
11
  import { entryPoints } from "./entry-points.js";
@@ -94,6 +93,7 @@ function childrenOf(held) {
94
93
  * is a root with everything its module exports: each name of the module's export
95
94
  * table, and where a name is a re-export, every link of the chain to the declaration
96
95
  * it carries, so a re-exported declaration is a root of the file that publishes it.
96
+ * In a library, what a published file exports is rooted by the published API alone.
97
97
  * A file a runtime executes without reading its exports, a test file or a worker, is
98
98
  * a root by itself.
99
99
  */
@@ -116,14 +116,19 @@ export function roots(project, held, targetRoot, options) {
116
116
  entered.push({ file, kind, source, exports });
117
117
  };
118
118
  const published = [];
119
- const publishes = (entry) => entry.published &&
119
+ const publishes = (entry) => options.publishedAPI &&
120
+ entry.published &&
120
121
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
122
+ for (const entry of options.manifest.entries) {
123
+ if (publishes(entry)) {
124
+ published.push(...files.named(entry.path));
125
+ }
126
+ }
127
+ const publishedNames = new Set(published.map((file) => file.fileName));
121
128
  for (const entry of options.manifest.entries) {
122
129
  for (const file of files.named(entry.path)) {
123
- enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
124
- if (options.publishedAPI && publishes(entry)) {
125
- published.push(file);
126
- }
130
+ const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
131
+ enter(file, kind, entry.member, !publishedNames.has(file.fileName));
127
132
  }
128
133
  }
129
134
  for (const pattern of options.entryFiles) {
@@ -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
  import { isArrayLiteralExpression, isArrowFunction, isCallExpression, isClassDeclaration, isClassExpression, isClassStaticBlockDeclaration, isFunctionDeclaration, isFunctionExpression, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isSetAccessorDeclaration, isSpreadElement, isStringLiteral, isVariableDeclaration, ModifierFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
11
12
  import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/unstable/sync";
@@ -20,6 +21,8 @@ import { valueReach } from "./value-reach.js";
20
21
  const JSON_STRINGIFY = { shape: "global", global: "JSON.stringify" };
21
22
  /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
22
23
  const CONVERSIONS = new Set(["toJSON", "toString"]);
24
+ /** The method a serializer calls on a value that carries one, wherever the value goes. */
25
+ const SERIALIZED_CONVERSION = "toJSON";
23
26
  /** The parameter types that keep nothing of the value they are given. */
24
27
  const ERASED = TypeFlags.Any | TypeFlags.Unknown;
25
28
  /** The wider of two reaches, where either may be absent. */
@@ -360,46 +363,62 @@ function flowsOf(project, held, sites, to) {
360
363
  const typeOf = new Map(asked.map((one, index) => [one.value, types[index]]));
361
364
  return arguments_.flatMap((one) => {
362
365
  const type = typeOf.get(one.value);
363
- let classes = new Set();
366
+ let reached = { classes: new Set(), members: [] };
364
367
  if (one.value.kind === SyntaxKind.ThisKeyword) {
365
- classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
368
+ reached = {
369
+ classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
370
+ members: [],
371
+ };
366
372
  }
367
373
  else if (type !== undefined) {
368
- classes = reach.classesOf(type);
374
+ reached = reach.reachedFrom(type);
369
375
  }
370
- const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
371
- return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
376
+ const carries = reached.classes.size > 0 || reached.members.length > 0;
377
+ const destination = carries ? to.at(one.site.call, one.argument) : undefined;
378
+ return destination === undefined
379
+ ? []
380
+ : [{ site: one.value, classes: reached.classes, members: reached.members, to: destination }];
372
381
  });
373
382
  }
374
383
  /**
375
- * The serialization-contract detector: each property of each class whose values reach
376
- * a destination, and the two conversion methods where the destination is outside the
377
- * analysis, recorded at the argument with the callee named.
384
+ * The serialization-contract detector: each data member and `toJSON` of each class,
385
+ * interface and object type a value that reaches a destination carries, and `toString`
386
+ * as well where the destination is outside the analysis, recorded at the argument with
387
+ * the callee named.
378
388
  */
379
389
  export function serializationContract(input) {
380
390
  const { project, held, targetRoot } = input;
381
391
  const walked = walkProject(project, held);
382
- const flows = flowsOf(project, held, walked.sites, destinations(input, aliasChains(project, held), walked));
392
+ const chains = aliasChains(project, held);
393
+ const flows = flowsOf(project, held, walked.sites, destinations(input, chains, walked));
383
394
  if (flows.length === 0) {
384
395
  return [];
385
396
  }
386
397
  const members = classMembers(project, held, new Set(flows.flatMap((flow) => [...flow.classes])), "instance");
387
398
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
399
+ // A destination holds back a data member and `toJSON`, and `toString` as well where
400
+ // the value left the analysis.
401
+ const holdsBack = (flags, name, to) => (flags & SymbolFlags.Property) !== 0 ||
402
+ ((flags & SymbolFlags.Method) !== 0 &&
403
+ (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
388
404
  const found = [];
389
405
  for (const flow of flows) {
390
406
  const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
391
407
  const detail = `passed to ${flow.to.callee}`;
392
408
  for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
393
409
  const symbol = byId.get(member.id);
394
- const isProperty = (member.flags & SymbolFlags.Property) !== 0;
395
- const converts = flow.to.reach === "conversions" &&
396
- (member.flags & SymbolFlags.Method) !== 0 &&
397
- symbol !== undefined &&
398
- CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
399
- if (isProperty || converts) {
410
+ const name = symbol === undefined ? "" : (memberComponent(symbol, byId) ?? "");
411
+ if (holdsBack(member.flags, name, flow.to)) {
400
412
  found.push({ id: member.id, detail, site });
401
413
  }
402
414
  }
415
+ for (const member of flow.members) {
416
+ if (holdsBack(member.flags, member.name, flow.to)) {
417
+ for (const id of chains.declarationsOf(member)) {
418
+ found.push({ id, detail, site });
419
+ }
420
+ }
421
+ }
403
422
  }
404
423
  return found;
405
424
  }
@@ -0,0 +1,32 @@
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
+ import { isArrowFunction, isClassDeclaration, isClassExpression, isFunctionDeclaration, isFunctionExpression, isMethodDeclaration, isPropertyDeclaration, isVariableDeclaration, } from "@typescript/native/unstable/ast";
6
+ /** Whether one node is a function value written in place: an arrow or a function expression. */
7
+ function isFunctionValue(node) {
8
+ return node !== undefined && (isArrowFunction(node) || isFunctionExpression(node));
9
+ }
10
+ /**
11
+ * The function one declaration declares: a function declaration and a class method with
12
+ * a body are their own; a variable and a class property whose initializer is an arrow or
13
+ * a function expression declare that function. Any other declaration declares none, an
14
+ * overload signature and an abstract or ambient method among them, because no body is
15
+ * there to read its parts.
16
+ */
17
+ export function signatureOf(node) {
18
+ if (isFunctionDeclaration(node)) {
19
+ return node.body === undefined ? undefined : node;
20
+ }
21
+ if (isMethodDeclaration(node)) {
22
+ const owner = node.parent;
23
+ return node.body !== undefined && (isClassDeclaration(owner) || isClassExpression(owner))
24
+ ? node
25
+ : undefined;
26
+ }
27
+ if ((isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
28
+ isFunctionValue(node.initializer)) {
29
+ return node.initializer;
30
+ }
31
+ return undefined;
32
+ }
@@ -105,6 +105,14 @@ function readRecord(value, site, byReference, shape) {
105
105
  }
106
106
  return { records: named.map((one) => ({ ...written, bound: one.id })), refusals: [] };
107
107
  }
108
+ function byReferenceOf(symbols) {
109
+ const byReference = new Map();
110
+ for (const symbol of symbols) {
111
+ const key = referenceKey(symbol.ref, symbol.position.path);
112
+ byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
113
+ }
114
+ return byReference;
115
+ }
108
116
  /** Reads one document at the target root, binding each record against the run's declarations. */
109
117
  function readDocument(host, targetRoot, symbols, shape) {
110
118
  const file = joinPath(targetRoot, shape.file);
@@ -139,11 +147,7 @@ function readDocument(host, targetRoot, symbols, shape) {
139
147
  if ((description !== undefined && typeof description !== "string") || !Array.isArray(held)) {
140
148
  return malformed(shape.file);
141
149
  }
142
- const byReference = new Map();
143
- for (const symbol of symbols) {
144
- const key = referenceKey(symbol.ref, symbol.position.path);
145
- byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
146
- }
150
+ const byReference = byReferenceOf(symbols);
147
151
  const records = [];
148
152
  const refusals = [];
149
153
  held.forEach((value, index) => {
@@ -172,9 +176,26 @@ export function readIgnoreFile(host, targetRoot, symbols) {
172
176
  export function readBaseline(host, targetRoot, symbols) {
173
177
  return readDocument(host, targetRoot, symbols, BASELINE_SHAPE);
174
178
  }
179
+ /**
180
+ * Rows a baseline write holds in memory, bound against the run's declarations as the same
181
+ * rows read back from the document would be, each carrying the reason given.
182
+ */
183
+ export function bindRows(rows, reason, symbols) {
184
+ const byReference = byReferenceOf(symbols);
185
+ const site = documentSite(BASELINE_SHAPE);
186
+ const read = rows.map((row) => readRecord({ ...row, reason }, site, byReference, BASELINE_SHAPE));
187
+ return {
188
+ records: read.flatMap((one) => one.records),
189
+ refusals: read.flatMap((one) => one.refusals),
190
+ };
191
+ }
175
192
  /** What a written document says about itself, so the same report writes the same bytes. */
176
193
  const BASELINE_DESCRIPTION = "Recorded findings: a run fails only on a finding this document does not hold. " +
177
194
  "Every reason is the analyzer that recorded the row, never an adjudication.";
195
+ /** The reason every row a baseline write records carries. */
196
+ export function rowReason(provenance) {
197
+ return `recorded by ${provenance.analyzer} ${provenance.version}`;
198
+ }
178
199
  /**
179
200
  * A baseline holding one row per finding, in the order given, which is the report's. Every
180
201
  * row carries the provenance as its reason. A finding missing a value a row needs, and an
@@ -185,7 +206,7 @@ export function writeBaseline(findings, provenance) {
185
206
  if (provenance.analyzer === "" || provenance.version === "") {
186
207
  throw new Error(`a baseline row would carry no reason: the analyzer identity names ${JSON.stringify(provenance.analyzer)} at version ${JSON.stringify(provenance.version)}`);
187
208
  }
188
- const reason = `recorded by ${provenance.analyzer} ${provenance.version}`;
209
+ const reason = rowReason(provenance);
189
210
  const baseline = findings.map((found) => {
190
211
  if (found.code === "" || found.symbol === "" || found.path === "") {
191
212
  throw new Error(`a baseline row would carry no reason: a finding names code ${JSON.stringify(found.code)}, symbol ${JSON.stringify(found.symbol)} and path ${JSON.stringify(found.path)}`);
@@ -6,6 +6,7 @@
6
6
  import { SyntaxKind } from "@typescript/native/unstable/ast";
7
7
  import { nodeKey } from "./inventory.js";
8
8
  import { byPosition, positionKey, renderPosition } from "./position.js";
9
+ import { signatureOf } from "./signatures.js";
9
10
  import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
10
11
  /**
11
12
  * The three expressions the grammar applies to the text of each line comment, in this
@@ -134,7 +135,8 @@ export function lineComments(file) {
134
135
  /**
135
136
  * The declarations of one file by the line each begins on, the line of its first token: a
136
137
  * decorator, a modifier or the keyword. A variable's first token is its statement's
137
- * keyword, and a variable written on a later line of the statement also begins there.
138
+ * keyword, and a variable written on a later line of the statement also begins there. A
139
+ * function also begins, for a directive, on the line of each of its parameters.
138
140
  */
139
141
  function declarationLines(file, held, symbols) {
140
142
  const below = new Map();
@@ -151,6 +153,11 @@ function declarationLines(file, held, symbols) {
151
153
  const one = id === undefined ? undefined : symbols.get(id);
152
154
  if (one !== undefined) {
153
155
  add(node.pos, one);
156
+ // A parameter on a later line of a wrapped signature takes its directive on the
157
+ // line above it, inside the parameter list, and the directive binds the function.
158
+ for (const parameter of signatureOf(node)?.parameters ?? []) {
159
+ add(parameter.pos, one);
160
+ }
154
161
  const list = node.parent;
155
162
  if (node.kind === SyntaxKind.VariableDeclaration &&
156
163
  list.kind === SyntaxKind.VariableDeclarationList &&
@@ -79,6 +79,7 @@ const ROW_SUBJECTS = new Set([
79
79
  "dependency",
80
80
  "module-directive",
81
81
  "root",
82
+ "configured-declaration",
82
83
  "suppression",
83
84
  "edge",
84
85
  ]);
@@ -86,6 +87,32 @@ const ROW_SUBJECTS = new Set([
86
87
  export function isRowSubject(kind) {
87
88
  return ROW_SUBJECTS.has(kind);
88
89
  }
90
+ /**
91
+ * The subject kinds that are a part of a declaration rather than a declaration: a part
92
+ * has no reference of its own, so a finding about one names the declaration that holds
93
+ * it, and the part is decided inside that declaration whatever uses it.
94
+ */
95
+ const PART_SUBJECTS = new Set([
96
+ "parameter",
97
+ "receiver",
98
+ "result",
99
+ "statement",
100
+ "case",
101
+ "store",
102
+ ]);
103
+ /** Whether a finding's subject is a part of the declaration its reference names. */
104
+ export function isPartSubject(kind) {
105
+ return PART_SUBJECTS.has(kind);
106
+ }
107
+ /** The prefix of the codes of the part kinds, the intra-function range. */
108
+ const PART_KIND_PREFIX = "DS18";
109
+ /**
110
+ * Whether a code is a part kind, whose record withholds its finding and marks nothing:
111
+ * the record says the part is wanted, not that the declaration holding it is.
112
+ */
113
+ export function isPartKind(code) {
114
+ return code.startsWith(PART_KIND_PREFIX);
115
+ }
89
116
  /** One finding's identity: its code and the position of the thing it names. */
90
117
  function claimKey(finding) {
91
118
  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
  import { ObjectFlags, SymbolFlags, } from "@typescript/native/unstable/sync";
9
11
  import { classNames, typesAt } from "./class-members.js";
@@ -11,6 +13,8 @@ import { classNames, typesAt } from "./class-members.js";
11
13
  const WRITTEN = ObjectFlags.ObjectLiteral | ObjectFlags.Mapped;
12
14
  /** The symbols of an anonymous object type the target writes as a type. */
13
15
  const LITERAL_SYMBOLS = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
16
+ /** The two conversion methods a serializer or a formatter resolves on a value's own shape. */
17
+ const CONVERSION_NAMES = new Set(["toJSON", "toString"]);
14
18
  /** The global array types, whose element type is what an array value carries. */
15
19
  function arrayTargets(project) {
16
20
  const checker = project.checker;
@@ -73,6 +77,22 @@ export function valueReach(project, held) {
73
77
  ...checker.getIndexInfosOfType(type).map((info) => info.valueType),
74
78
  ];
75
79
  };
80
+ /** Whether the walk reads one type's members because the target writes the type, not as a class. */
81
+ const writtenObject = (type) => {
82
+ if (!type.isObjectType() || classOf(type) !== undefined) {
83
+ return false;
84
+ }
85
+ const target = targetOf(type);
86
+ const isList = type.isTypeReference() &&
87
+ (arrays.has(target.id) ||
88
+ (target.isObjectType() && (target.objectFlags & ObjectFlags.Tuple) !== 0));
89
+ return !isList && isWritten(type);
90
+ };
91
+ /** The data members and conversion methods of one written object type. */
92
+ const carried = (type) => checker
93
+ .getPropertiesOfType(type)
94
+ .filter((property) => (property.flags & SymbolFlags.Property) !== 0 ||
95
+ ((property.flags & SymbolFlags.Method) !== 0 && CONVERSION_NAMES.has(property.name)));
76
96
  /** The types a value of one type holds, read once per type. */
77
97
  const childrenOf = (type) => {
78
98
  const known = children.get(type.id);
@@ -99,8 +119,9 @@ export function valueReach(project, held) {
99
119
  return found;
100
120
  };
101
121
  return {
102
- classesOf: (type) => {
103
- const found = new Set();
122
+ reachedFrom: (type) => {
123
+ const classes = new Set();
124
+ const members = [];
104
125
  const seen = new Set();
105
126
  const pending = [type];
106
127
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
@@ -110,11 +131,14 @@ export function valueReach(project, held) {
110
131
  seen.add(next.id);
111
132
  const owner = classOf(next);
112
133
  if (owner !== undefined) {
113
- found.add(owner);
134
+ classes.add(owner);
135
+ }
136
+ else if (writtenObject(next)) {
137
+ members.push(...carried(next));
114
138
  }
115
139
  pending.push(...childrenOf(next));
116
140
  }
117
- return found;
141
+ return { classes, members };
118
142
  },
119
143
  };
120
144
  }
@@ -1,15 +1,15 @@
1
- import { runAnalysis } from "../analysis.js";
1
+ import { runAnalysis, runBaselineWrite } from "../analysis.js";
2
2
  import { ConfigError } from "../config.js";
3
3
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
4
4
  import { moduleIdentity } from "../consumers.js";
5
- import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.js";
5
+ import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
6
  import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
7
  import { buildReport, capFindings, sortFindings, } from "../report.js";
8
8
  import { json } from "../reporters/json.js";
9
9
  import { RENDERINGS } from "../reporters/reporters.js";
10
10
  import { parseTemplate, TemplateError } from "../reporters/template.js";
11
11
  import { resolve } from "../resolve.js";
12
- import { writeBaseline } from "../suppress-file.js";
12
+ import { rowReason, writeBaseline } from "../suppress-file.js";
13
13
  import { CONTRACT_VERSION } from "../version.js";
14
14
  import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
15
15
  /** The options `analyze` takes beside the ones every verb takes. */
@@ -107,8 +107,9 @@ function templateOf(host, invoked, named, formats) {
107
107
  /**
108
108
  * Analyzes the scope under the configuration and writes the JSON report to the path
109
109
  * `--report` names, each rendering `--format` names beside it at the report's path with the
110
- * format's suffix appended, and a baseline of every finding where `--baseline-write` names a
111
- * path. The template format renders the template `--template` names. Nothing is written to
110
+ * format's suffix appended, and where `--baseline-write` names a path the baseline a fixpoint
111
+ * over the analysis records, the report then being its first round, which reads no
112
+ * baseline. The template format renders the template `--template` names. Nothing is written to
112
113
  * the output stream; diagnostics go to the error stream and the verdict is the exit code, or 0
113
114
  * under `--exit-code=off`, and a rendering that cannot be written ends the run with 3.
114
115
  */
@@ -149,9 +150,19 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
149
150
  let verdict;
150
151
  try {
151
152
  const root = runRelative(invoked, targetRoot);
152
- const { result, run } = runAnalysis(openClient(false), host, scoped, config, provenance, {
153
- production: true,
154
- });
153
+ const mode = { production: true };
154
+ let analysis;
155
+ if (documents.baseline === undefined) {
156
+ analysis = runAnalysis(openClient(false), host, scoped, config, provenance, mode);
157
+ recorded = [];
158
+ }
159
+ else {
160
+ const reason = rowReason(analyzerProvenance(version));
161
+ const write = runBaselineWrite(openClient(false), host, scoped, config, provenance, mode, reason);
162
+ analysis = write.first;
163
+ recorded = write.rows;
164
+ }
165
+ const { result, run } = analysis;
155
166
  const built = buildReport({
156
167
  contractVersion: CONTRACT_VERSION,
157
168
  version,
@@ -167,9 +178,6 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
167
178
  result,
168
179
  testFileRules: run.testFileRules,
169
180
  });
170
- // The baseline records every finding, so its rows are taken before the cap bounds what
171
- // a rendering prints: a baseline missing a reported finding fails the next run on it.
172
- recorded = recordedFindings(result);
173
181
  report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
174
182
  verdict = verdictOf(result, config);
175
183
  }
@@ -1,24 +1,28 @@
1
1
  import { CONFORMANCE } from "../conformance.js";
2
- import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSION } from "../report.js";
2
+ import { ANALYZER_NAME, LANGUAGE, SCHEMA_VERSIONS_ACCEPTED } from "../report.js";
3
3
  import { CONTRACT_VERSION } from "../version.js";
4
4
  import { EXIT_CLEAN, EXIT_FAILURE, EXIT_USAGE } from "./verb.js";
5
5
  /**
6
6
  * The document `describe` writes: the analyzer, the Contract version it implements, the
7
7
  * report schema versions it reads, the languages it claims, and its recorded result over
8
- * the conformance corpus.
8
+ * the conformance corpus, a member the document omits where no run is recorded.
9
9
  */
10
10
  export function describeDocument(version, recorded) {
11
11
  const document = {
12
12
  name: ANALYZER_NAME,
13
13
  version,
14
14
  contract_version: CONTRACT_VERSION,
15
- schema_versions_accepted: [SCHEMA_VERSION],
15
+ schema_versions_accepted: SCHEMA_VERSIONS_ACCEPTED,
16
16
  languages: [LANGUAGE],
17
- conformance: {
18
- corpus_version: recorded.corpusVersion,
19
- result: recorded.result,
20
- digest: recorded.digest,
21
- },
17
+ ...(recorded === undefined
18
+ ? {}
19
+ : {
20
+ conformance: {
21
+ corpus_version: recorded.corpusVersion,
22
+ result: recorded.result,
23
+ digest: recorded.digest,
24
+ },
25
+ }),
22
26
  };
23
27
  return `${JSON.stringify(document, null, 2)}\n`;
24
28
  }
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "3.2.0";
7
+ export const CONTRACT_VERSION = "4.0.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",