@cplieger/deadset-ts 3.8.0 → 3.9.1

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
8
8
 
9
9
  deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
10
10
 
11
- **Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
11
+ **Status: pre-release.** This version implements contract 3.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. Seven exemption classes hold a declaration back from the sweep, and `print-retained` lists what each one held back: `interface-satisfaction`, a member an interface requires of a class whose value reaches that interface; `decorator`, a decorated member and every member of a decorated class; `injection-container`, a decorated property of a class a dependency-injection container constructs, being a class passed to a call `ts.injection_registrations` names or one a parameter of whose constructor carries a decorator; `framework-lifecycle`, a member a `ts.lifecycle_contracts` entry lists, on a class that is the entry's component by a decorator or a call the entry names or by extending a class it names; `serialization-contract`, the properties of a class whose values reach `JSON.stringify` or a declaration `ts.serializers` names, and those and its `toJSON` and `toString` where a value is passed to an `unknown` or `any` parameter of a function outside the analysis; and, at the lowest confidence, `template-field`, a member an action of a template under `analysis.template_dirs` names, and `reflective-lookup`, a member a literal key of an element access or of `Reflect.get`, `Reflect.set` or `Reflect.has` names. None of them holds back a `#private` member. `analyze` writes the report the Contract's report schema declares, with a text or JSON rendering beside it on request, and `describe` prints the analyzer's description, which states no conformance record, so an orchestrator that requires a conformance pass refuses this analyzer. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; `explain` exits with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
12
12
 
13
13
  ## Install
14
14
 
@@ -28,18 +28,18 @@ The npm package carries the command and the library as JavaScript that the compi
28
28
  npx deadset-ts version
29
29
  ```
30
30
 
31
- `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze`, `explain`, `describe`, any other invocation and no arguments print the usage line and exit 2.
32
-
33
- | Verb | Job |
34
- | ---------------- | ------------------------------------------------------------------------------- |
35
- | `analyze` | Analyze a project and print the findings |
36
- | `explain` | Explain one finding: the relation, the roots and the references that decided it |
37
- | `print-config` | Print the resolved configuration and where each setting came from |
38
- | `print-projects` | Print the identifier of every project a run analyzes |
39
- | `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
40
- | `print-retained` | Print every declaration an exemption held back: each class, its site and detail |
41
- | `describe` | Describe the analyzer: version, Contract version, supported issue kinds |
42
- | `version` | Print the analyzer and Contract versions |
31
+ `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class or an `analysis.template_dirs` entry is not a directory below the target root. `analyze --report=PATH` writes the JSON report to `PATH` through a temporary file renamed into place, each `--format=text` or `--format=json` rendering at `PATH` with `.txt` or `.json` appended, and with `--baseline-write=FILE` a baseline of every finding; it writes nothing to standard output and exits with the Contract's exit-code table: 0 clean, 1 for a finding at or above `reporters.fail_on` or a stale suppression, 2 for a usage error, 3 for a failed load, and 4 for a pending finding, which outranks 1. `--exit-code=off` exits 0 whatever the verdict and names the verdict on standard error. The report names the target relative to the directory the run is invoked from, so a target outside that directory fails with 3. `describe` writes its JSON document to standard output and exits 0. `explain`, any other invocation and no arguments print the usage line and exit 2.
32
+
33
+ | Verb | Job |
34
+ | ---------------- | -------------------------------------------------------------------------------- |
35
+ | `analyze` | Analyze a project and write the report, its renderings and a baseline |
36
+ | `explain` | Explain one finding: the relation, the roots and the references that decided it |
37
+ | `print-config` | Print the resolved configuration and where each setting came from |
38
+ | `print-projects` | Print the identifier of every project a run analyzes |
39
+ | `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
40
+ | `print-retained` | Print every declaration an exemption held back: each class, its site and detail |
41
+ | `describe` | Describe the analyzer: versions, accepted report schemas, languages, conformance |
42
+ | `version` | Print the analyzer and Contract versions |
43
43
 
44
44
  ## Requirements
45
45
 
@@ -1,5 +1,6 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { randomBytes } from "node:crypto";
2
+ import { closeSync, fchmodSync, fsyncSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  /** The name the manifest of this package declares. */
5
6
  const PACKAGE_NAME = "@cplieger/deadset-ts";
@@ -43,6 +44,34 @@ function readVersion() {
43
44
  at = up;
44
45
  }
45
46
  }
47
+ /** The mode every document the command writes carries. */
48
+ const DOCUMENT_MODE = 0o644;
49
+ /**
50
+ * Writes one document through a temporary file in the same directory, flushed and then
51
+ * renamed into place, so a run that dies partway leaves no truncated document at the path.
52
+ */
53
+ function writeAtomically(path, text) {
54
+ const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}`);
55
+ const fd = openSync(temporary, "wx", DOCUMENT_MODE);
56
+ try {
57
+ try {
58
+ fchmodSync(fd, DOCUMENT_MODE);
59
+ const bytes = Buffer.from(text, "utf8");
60
+ for (let written = 0; written < bytes.length;) {
61
+ written += writeSync(fd, bytes, written);
62
+ }
63
+ fsyncSync(fd);
64
+ }
65
+ finally {
66
+ closeSync(fd);
67
+ }
68
+ renameSync(temporary, path);
69
+ }
70
+ catch (error) {
71
+ rmSync(temporary, { force: true });
72
+ throw error;
73
+ }
74
+ }
46
75
  /**
47
76
  * The filesystem of the platform the command runs on, and the version of the
48
77
  * package it was installed from.
@@ -70,5 +99,6 @@ export function nodeHost() {
70
99
  found ??= readVersion();
71
100
  return found;
72
101
  },
102
+ writeFile: writeAtomically,
73
103
  };
74
104
  }
@@ -27,7 +27,7 @@ import { matrixOf, sweepMatrix } from "./matrix.js";
27
27
  import { relativePath, resolvePath } from "./paths.js";
28
28
  import { byPosition } from "./position.js";
29
29
  import { reflectiveLookup } from "./reflective-lookup.js";
30
- import { references } from "./references.js";
30
+ import { references, testFileRulesOf } from "./references.js";
31
31
  import { roots, unmatchedEverywhere } from "./roots.js";
32
32
  import { serializationContract } from "./serialization-contract.js";
33
33
  import { diagnosticsOf, runSession } from "./session.js";
@@ -85,7 +85,7 @@ function readProjects(engine, host, scope, config, stage) {
85
85
  const held = inventory(project, host, targetRoot);
86
86
  const rooted = roots(project, held, targetRoot, options);
87
87
  const configuration = ids.get(project.configFile) ?? project.configFile;
88
- return stage(project, { configuration, held, rooted });
88
+ return stage(project, { configuration, configFile: project.configFile, held, rooted });
89
89
  });
90
90
  if (failures.length > 0) {
91
91
  throw new DiscoveryError(`${String(failures.length)} error(s); no answer was produced`, failures);
@@ -175,7 +175,12 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
175
175
  mode,
176
176
  testFiles: new Set(resolved.testFilePaths),
177
177
  });
178
- return { configured, exempt, extra: extra(project, projectRead) };
178
+ return {
179
+ configFile: projectRead.configFile,
180
+ configured,
181
+ exempt,
182
+ extra: extra(project, projectRead),
183
+ };
179
184
  });
180
185
  return {
181
186
  matrix: matrixOf(projects.map((one) => one.configured)),
@@ -265,8 +270,9 @@ export function runEmitterInput(engine, host, scope, config, input, detectors =
265
270
  * any sweep, and the run is swept once under every bound record's mark and once under
266
271
  * none, so a record is decided against the finding its code would have produced. The
267
272
  * self-check family reports the refused suppressions and the roots that named nothing.
273
+ * Beside the inputs it answers the projects and test-file rules a report names.
268
274
  */
269
- function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
275
+ function runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors) {
270
276
  const targetRoot = scope.target.path;
271
277
  const read = readEmitterRun(engine, host, scope, config, mode, detectors);
272
278
  const symbols = read.matrix.union.symbols;
@@ -286,16 +292,26 @@ function runAnalysisInputs(engine, host, scope, config, provenance, mode, detect
286
292
  };
287
293
  const marked = [...new Set(suppressions.records.map((record) => record.bound))].filter((id) => id !== "");
288
294
  return {
289
- marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
290
- unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
291
- suppressions,
295
+ inputs: {
296
+ marked: emitterInputOver(host, scope, config, read, { marked, mode }, facts),
297
+ unmarked: emitterInputOver(host, scope, config, read, { marked: [], mode }, facts),
298
+ suppressions,
299
+ },
300
+ run: {
301
+ projects: read.projects.map((one) => ({
302
+ id: one.configured.configuration,
303
+ configFile: one.configFile,
304
+ })),
305
+ testFileRules: testFileRulesOf(config.ts.testFiles, read.projects.flatMap((one) => one.configured.testFiles)),
306
+ },
292
307
  };
293
308
  }
294
309
  /**
295
- * The findings of the run the scope and the configuration describe: the run read with its
296
- * suppressions bound, then every family's findings with the suppressions applied and the
297
- * declared edges evaluated.
310
+ * The analysis of the run the scope and the configuration describe: the run read with its
311
+ * suppressions bound, every family's findings with the suppressions applied and the
312
+ * declared edges evaluated, and the projects and test-file rules the report names.
298
313
  */
299
- export function runFindings(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
300
- return findingsPass(runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors));
314
+ export function runAnalysis(engine, host, scope, config, provenance, mode, detectors = DETECTORS) {
315
+ const { inputs, run } = runAnalysisInputs(engine, host, scope, config, provenance, mode, detectors);
316
+ return { result: findingsPass(inputs), run };
301
317
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * What this analyzer states about its own conformance: the corpus version it is written
3
+ * against and its recorded result over that corpus, which `describe` and every report name.
4
+ */
5
+ /** The version of the conformance corpus this analyzer is written against. */
6
+ const CORPUS_VERSION = "1.9.0";
7
+ /** The SHA-256 of zero bytes, which is the digest of a results document no run wrote. */
8
+ const EMPTY_DIGEST = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
9
+ /**
10
+ * The conformance run this analyzer records, or undefined where it records none. A
11
+ * description states exactly this, so a reader that requires a pass refuses an analyzer
12
+ * that has not answered the corpus.
13
+ */
14
+ export const RECORDED_CONFORMANCE = undefined;
15
+ /**
16
+ * The conformance a report states. A report requires the block, so an analyzer that
17
+ * records no run states a failed one over the results document no run wrote, which a merge
18
+ * refuses as it refuses any result other than a pass.
19
+ */
20
+ export function reportedConformance(recorded) {
21
+ return recorded ?? { corpusVersion: CORPUS_VERSION, result: "fail", digest: EMPTY_DIGEST };
22
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The members a destructuring reads. An object pattern reads each property it names
3
+ * from the value it destructures, so the property's name in the pattern is a read of
4
+ * the member the value's type declares under that name, whether the pattern binds it
5
+ * to a local of the same name, renames it, gives it a default or nests a pattern under
6
+ * it. A rest element names no property: what it copies is read where the copy is.
7
+ */
8
+ import { isArrayLiteralExpression, isBinaryExpression, isForOfStatement, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAssignment, isShorthandPropertyAssignment, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { TypeFlags } from "@typescript/native/unstable/sync";
10
+ /** The text a property name spells, or `undefined` for a computed name. */
11
+ function nameText(node) {
12
+ if (node === undefined) {
13
+ return undefined;
14
+ }
15
+ if (isIdentifier(node) ||
16
+ isStringLiteral(node) ||
17
+ isNoSubstitutionTemplateLiteral(node) ||
18
+ isNumericLiteral(node)) {
19
+ return node.text;
20
+ }
21
+ return undefined;
22
+ }
23
+ /** The node one expression is written inside, through however many parentheses enclose it. */
24
+ function container(node) {
25
+ let child = node;
26
+ while (isParenthesizedExpression(child.parent)) {
27
+ child = child.parent;
28
+ }
29
+ return { child, parent: child.parent };
30
+ }
31
+ /** The plain assignment one node is the left side of, if it is one. */
32
+ function assignmentOf(node, parent) {
33
+ return isBinaryExpression(parent) &&
34
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
35
+ parent.left === node
36
+ ? parent
37
+ : undefined;
38
+ }
39
+ /**
40
+ * Whether an object or array literal is a destructuring target rather than a value: the
41
+ * left side of an assignment, the target of a `for…of`, or an element of a literal that
42
+ * is one, with or without a default.
43
+ */
44
+ function isDestructuringTarget(literal) {
45
+ const { child, parent } = container(literal);
46
+ if (assignmentOf(child, parent) !== undefined) {
47
+ return true;
48
+ }
49
+ if (isForOfStatement(parent)) {
50
+ return parent.initializer === child;
51
+ }
52
+ return isElement(child, parent);
53
+ }
54
+ /** Whether one node is an element of a destructuring target: a property's value, or an array element. */
55
+ function isElement(child, parent) {
56
+ if (isPropertyAssignment(parent)) {
57
+ return parent.initializer === child && isDestructuringTarget(parent.parent);
58
+ }
59
+ return isArrayLiteralExpression(parent) && isDestructuringTarget(parent);
60
+ }
61
+ /**
62
+ * Whether an object binding pattern runs: a pattern in the parameter list of a signature
63
+ * with no body, a function type or an overload among them, binds nothing at runtime.
64
+ */
65
+ function binds(pattern) {
66
+ let at = pattern.parent;
67
+ while (at.kind === SyntaxKind.BindingElement) {
68
+ at = at.parent.parent;
69
+ }
70
+ if (!isParameterDeclaration(at)) {
71
+ return true;
72
+ }
73
+ return at.parent.body !== undefined;
74
+ }
75
+ /** The properties one node reads, where it is an object pattern; none otherwise. */
76
+ export function propertyReadsOf(node) {
77
+ const reads = [];
78
+ if (isObjectBindingPattern(node)) {
79
+ if (!binds(node)) {
80
+ return reads;
81
+ }
82
+ for (const element of node.elements) {
83
+ const key = element.propertyName ?? element.name;
84
+ const name = nameText(key);
85
+ if (element.dotDotDotToken === undefined && key !== undefined && name !== undefined) {
86
+ reads.push({ pattern: node, key, name });
87
+ }
88
+ }
89
+ return reads;
90
+ }
91
+ if (isObjectLiteralExpression(node) && isDestructuringTarget(node)) {
92
+ for (const property of node.properties) {
93
+ if (isShorthandPropertyAssignment(property) || isPropertyAssignment(property)) {
94
+ const name = nameText(property.name);
95
+ if (name !== undefined) {
96
+ reads.push({ pattern: node, key: property.name, name });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return reads;
102
+ }
103
+ /**
104
+ * Where the value one destructuring target receives comes from. The checker answers for
105
+ * a binding pattern's type itself, so this is asked of an assignment target only, whose
106
+ * own type is the literal's rather than the assigned value's. A default does not move
107
+ * the source: `{ a: { b } = fallback }` reads `b` from `a`. A target inside a spread
108
+ * takes the rest of a value and has no source here.
109
+ */
110
+ function sourceOf(target) {
111
+ const steps = [];
112
+ let at = target;
113
+ for (;;) {
114
+ const { child, parent } = container(at);
115
+ const assignment = assignmentOf(child, parent);
116
+ if (assignment !== undefined) {
117
+ const outer = container(assignment);
118
+ if (!isElement(outer.child, outer.parent)) {
119
+ return { expression: assignment.right, steps: steps.reverse() };
120
+ }
121
+ at = assignment;
122
+ continue;
123
+ }
124
+ if (isForOfStatement(parent) && parent.initializer === child) {
125
+ steps.push({ kind: "iterated" });
126
+ return { expression: parent.expression, steps: steps.reverse() };
127
+ }
128
+ if (isPropertyAssignment(parent) && parent.initializer === child) {
129
+ const name = nameText(parent.name);
130
+ if (name === undefined) {
131
+ return undefined;
132
+ }
133
+ steps.push({ kind: "property", name });
134
+ at = parent.parent;
135
+ continue;
136
+ }
137
+ if (isArrayLiteralExpression(parent)) {
138
+ steps.push({
139
+ kind: "element",
140
+ index: parent.elements.findIndex((element) => element === child),
141
+ });
142
+ at = parent;
143
+ continue;
144
+ }
145
+ return undefined;
146
+ }
147
+ }
148
+ /**
149
+ * The resolution over one project, its batches capped at `cap`. Property tables are
150
+ * read once per type for the whole project, since one type is destructured in many
151
+ * places and a table answers every name of it.
152
+ */
153
+ export function destructuring(project, cap) {
154
+ const checker = project.checker;
155
+ const tables = new Map();
156
+ let patternBatches = 0;
157
+ let patternLookups = 0;
158
+ const typesAt = (nodes) => {
159
+ const types = [];
160
+ for (let from = 0; from < nodes.length; from += cap) {
161
+ types.push(...checker.getTypeAtLocation(nodes.slice(from, from + cap)));
162
+ patternBatches += 1;
163
+ }
164
+ return types;
165
+ };
166
+ const tableOf = (type) => {
167
+ const known = tables.get(type.id);
168
+ if (known !== undefined) {
169
+ return known;
170
+ }
171
+ const table = new Map();
172
+ if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
173
+ patternLookups += 1;
174
+ for (const property of checker.getPropertiesOfType(type)) {
175
+ table.set(property.name, property);
176
+ }
177
+ }
178
+ tables.set(type.id, table);
179
+ return table;
180
+ };
181
+ const definedOf = (type) => {
182
+ if ((type.flags & TypeFlags.Union) === 0) {
183
+ return type;
184
+ }
185
+ patternLookups += 1;
186
+ return checker.getNonNullableType(type);
187
+ };
188
+ /**
189
+ * The type one element of an array or tuple value has: a tuple's element at `index`,
190
+ * an array's element type at any index, and `undefined` for a tuple iterated whole.
191
+ */
192
+ const elementOf = (type, index) => {
193
+ if (!type.isTypeReference()) {
194
+ return undefined;
195
+ }
196
+ patternLookups += 1;
197
+ if (checker.isTupleType(type)) {
198
+ if (index === undefined) {
199
+ return undefined;
200
+ }
201
+ patternLookups += 1;
202
+ return checker.getTypeArguments(type)[index];
203
+ }
204
+ patternLookups += 1;
205
+ if (!checker.isArrayType(type)) {
206
+ return undefined;
207
+ }
208
+ patternLookups += 1;
209
+ return checker.getTypeArguments(type)[0];
210
+ };
211
+ const step = (type, next) => {
212
+ const defined = definedOf(type);
213
+ if (defined === undefined) {
214
+ return undefined;
215
+ }
216
+ if (next.kind === "property") {
217
+ const property = tableOf(defined).get(next.name);
218
+ if (property === undefined) {
219
+ return undefined;
220
+ }
221
+ patternLookups += 1;
222
+ return checker.getTypeOfSymbol(property);
223
+ }
224
+ return elementOf(defined, next.kind === "element" ? next.index : undefined);
225
+ };
226
+ const resolve = (reads) => {
227
+ const patterns = [...new Set(reads.map((read) => read.pattern))];
228
+ const sources = new Map();
229
+ const asking = new Set();
230
+ for (const pattern of patterns) {
231
+ if (isObjectBindingPattern(pattern)) {
232
+ asking.add(pattern);
233
+ continue;
234
+ }
235
+ const source = sourceOf(pattern);
236
+ sources.set(pattern, source);
237
+ if (source !== undefined) {
238
+ asking.add(source.expression);
239
+ }
240
+ }
241
+ const answered = new Map();
242
+ const asked = [...asking];
243
+ typesAt(asked).forEach((type, index) => {
244
+ const node = asked[index];
245
+ if (type !== undefined && node !== undefined) {
246
+ answered.set(node, type);
247
+ }
248
+ });
249
+ const patternTypes = new Map();
250
+ for (const pattern of patterns) {
251
+ const source = sources.get(pattern);
252
+ if (source === undefined) {
253
+ patternTypes.set(pattern, answered.get(pattern));
254
+ continue;
255
+ }
256
+ let type = answered.get(source.expression);
257
+ for (const next of source.steps) {
258
+ type = type === undefined ? undefined : step(type, next);
259
+ }
260
+ patternTypes.set(pattern, type === undefined ? undefined : definedOf(type));
261
+ }
262
+ return reads.map((read) => {
263
+ const type = patternTypes.get(read.pattern);
264
+ return type === undefined ? undefined : tableOf(type).get(read.name);
265
+ });
266
+ };
267
+ return {
268
+ resolve,
269
+ get cost() {
270
+ return { patternBatches, patternLookups };
271
+ },
272
+ };
273
+ }
@@ -7,6 +7,7 @@ import { componentId } from "../components.js";
7
7
  import { OUTSIDE } from "../graph.js";
8
8
  import { KINDS } from "../kinds.js";
9
9
  import { FAMILY_KEY_LENGTH } from "../resolve.js";
10
+ import { isRowSubject } from "../suppress.js";
10
11
  /** The classes ranked by the claim each makes, the strongest highest. */
11
12
  const RANK = { certain: 3, probable: 2, possible: 1 };
12
13
  /** The weaker of two classes, which is a class capped by a ceiling. */
@@ -59,7 +60,8 @@ function findingComponents(swept) {
59
60
  * candidate's relation, counts, configurations and component; a live one carries no
60
61
  * relation and holds where it is declared; a file or a manifest row is `certain` and
61
62
  * holds in every configuration. A subject in no dead component gets one of its own,
62
- * numbered past the computed ones in finding order, so no two families mint one.
63
+ * numbered past the computed ones in finding order, so no two families mint one; nothing
64
+ * falls with it, so its deletion removes no line.
63
65
  */
64
66
  export function completed(input, findings) {
65
67
  const { swept } = input;
@@ -74,7 +76,11 @@ export function completed(input, findings) {
74
76
  if (row === undefined) {
75
77
  throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
76
78
  }
77
- const at = positions.get(finding.symbol.ref) ?? OUTSIDE;
79
+ // A row's reference may spell a declaration's, as an ignore entry naming one does, and
80
+ // a row is still never the declaration.
81
+ const at = isRowSubject(finding.symbol.kind)
82
+ ? OUTSIDE
83
+ : (positions.get(finding.symbol.ref) ?? OUTSIDE);
78
84
  const symbol = union.symbols[at];
79
85
  const judged = union.subject[at] === true ? symbol : undefined;
80
86
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
@@ -89,7 +95,7 @@ export function completed(input, findings) {
89
95
  id: componentId(minted),
90
96
  root: true,
91
97
  symbolCount: 1,
92
- deletableLines: finding.symbol.sizeLines,
98
+ deletableLines: 0,
93
99
  };
94
100
  }
95
101
  const counts = candidate === undefined
@@ -35,12 +35,24 @@ function spelled(codes) {
35
35
  return `${codes.slice(0, -1).join(", ")} and ${codes.at(-1) ?? ""}`;
36
36
  }
37
37
  /** A finding about one suppression, at the suppression's own site. */
38
- function aboutSuppression(code, ref, site, message) {
38
+ function aboutSuppression(code, ref, site, message, details) {
39
39
  return {
40
40
  code,
41
41
  position: { ...site, endLine: site.line },
42
42
  symbol: { ref, kind: SUPPRESSION_SUBJECT, name: ref, sizeLines: 1 },
43
43
  message,
44
+ details,
45
+ };
46
+ }
47
+ /** Whether a reason holds a character other than whitespace. */
48
+ const STATED = /[^ \t\r\n]/u;
49
+ /** A refused suppression as written: a member it lacks is absent. */
50
+ function writtenEntry(refused) {
51
+ return {
52
+ code: refused.code,
53
+ ...(refused.symbol === "" ? {} : { symbol: refused.symbol }),
54
+ ...(refused.path === "" ? {} : { path: refused.path }),
55
+ ...(STATED.test(refused.reason) ? { reason: refused.reason } : {}),
44
56
  };
45
57
  }
46
58
  /**
@@ -54,7 +66,7 @@ function suppressionRef(symbol, path, files) {
54
66
  export function refusedSuppressions(refusals, files) {
55
67
  return refusals.map((refused) => aboutSuppression(refused.reported, suppressionRef(refused.symbol, refused.path, files), refused.site, `${WRITTEN[refused.mechanism]} for ${refused.code} ${refused.reported === SUPPRESSION_WITHOUT_REASON
56
68
  ? "carries no reason"
57
- : "names a symbol and no path"}`));
69
+ : "names a symbol and no path"}`, { mechanism: refused.mechanism, entry: writtenEntry(refused) }));
58
70
  }
59
71
  /**
60
72
  * One record per site at which a suppression matched nothing, whatever the number of