@cplieger/deadset-ts 8.0.0-dev.3 → 8.0.0-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
7
- It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261010.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
7
+ It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261014.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
8
8
 
9
9
  ## Why use it
10
10
 
@@ -20,7 +20,7 @@ import { enumGroup } from "./enum-group.js";
20
20
  import { generatedFile, generatedFilesOf } from "./generated-file.js";
21
21
  import { inOperandNames } from "./in-operands.js";
22
22
  import { sourceFilesOf } from "./source-files.js";
23
- import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
+ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, writtenRecords, } from "./exempt.js";
24
24
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
25
25
  import { globExpression } from "./glob.js";
26
26
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
@@ -41,7 +41,7 @@ import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.
41
41
  import { reflectiveLookup } from "./reflective-lookup.js";
42
42
  import { references, testFileRulesOf } from "./references.js";
43
43
  import { roots, unmatchedEverywhere, } from "./roots.js";
44
- import { serializationContract } from "./serialization-contract.js";
44
+ import { decodedMembers, serializationContract } from "./serialization-contract.js";
45
45
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
46
46
  import { mergedGlobalInterfaces } from "./augmentations.js";
47
47
  import { unseenLoadsOf } from "./unseen-loads.js";
@@ -534,6 +534,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
534
534
  line: lineOf(file.originalText, warning.offset),
535
535
  reason: warning.reason,
536
536
  }))),
537
+ decoded: decodedMembers(project, projectRead.held, productionFiles(project, targetRoot, resolved.testFilePaths), consumers),
537
538
  extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
538
539
  };
539
540
  });
@@ -549,11 +550,24 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
549
550
  // Test-support code is code only test code imports in any configuration of the run: a
550
551
  // configuration that holds no test file says nothing about what the tests import.
551
552
  const { support, testUsed } = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
552
- const settled = settledSatisfaction(withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
553
+ // A member a decode fills or a copy carries is written: a value decoded and sent again carries it on.
554
+ const written = new Set([
555
+ ...countedWrites([...withOutside.flatMap((one) => one.references), ...read.consumers.references], support, mode),
556
+ ...withOutside.flatMap((one) => [
557
+ ...one.decoded.filled,
558
+ ...one.decoded.parsed,
559
+ ...one.decoded.copied,
560
+ ]),
561
+ ]);
562
+ const evidence = withOutside.map((one) => writtenRecords(one.evidence, written));
563
+ const settled = settledSatisfaction(withOutside.map((one, index) => ({
564
+ evidence: evidence[index] ?? [],
565
+ held: one.projectRead.held,
566
+ })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
553
567
  const answers = withOutside.map((one, index) => {
554
568
  const { projectRead } = one;
555
569
  const satisfied = settled[index];
556
- const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
570
+ const detected = heldRecords(satisfied?.evidence ?? evidence[index] ?? [], {
557
571
  mode,
558
572
  testFiles: new Set([...one.testFiles, ...support]),
559
573
  });
@@ -591,7 +605,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
591
605
  .filter((reference) => reference.resolution === "by-name")
592
606
  .map((reference) => reference.to),
593
607
  ...exempt.filter(isUnansweredRecord).map((record) => record.id),
608
+ ...one.decoded.unanswered,
594
609
  ],
610
+ filledOutside: one.decoded.filled,
595
611
  typeErrorSkips: projectRead.typeErrorSkips,
596
612
  unmappedEntries: projectRead.rooted.unmappedEntries,
597
613
  skipped: projectRead.skipped,
@@ -681,6 +697,13 @@ function evidenceReferences(symbols, inTestFiles) {
681
697
  ];
682
698
  });
683
699
  }
700
+ /**
701
+ * The declarations a write the mode counts stores into, a write a test-support file
702
+ * makes being a test's.
703
+ */
704
+ function countedWrites(references, support, mode) {
705
+ return new Set(supportReferences(references, support).flatMap((reference) => reference.use === "write" && !(mode.production && reference.test) ? [reference.to] : []));
706
+ }
684
707
  /**
685
708
  * The declarations every reference the mode counts stores into, a loaded consumer's
686
709
  * reference counting as a read: what the write-only kind would report but for a record.
@@ -765,6 +788,11 @@ function sweptOf(read, input) {
765
788
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
766
789
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
767
790
  }
791
+ /** The project's own files below the target root that are no test file. */
792
+ function productionFiles(project, targetRoot, testFiles) {
793
+ const tests = new Set(testFiles);
794
+ return [...sourceFilesOf(project, targetRoot).byPath].flatMap(([path, file]) => tests.has(path) ? [] : [file]);
795
+ }
768
796
  /** Reads a run's projects with every per-project fact an emitter reads. */
769
797
  export function readEmitterRun(engine, host, scope, config, mode, detectors) {
770
798
  const targetRoot = scope.target.path;
@@ -782,7 +810,7 @@ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
782
810
  parts: projectParts(project, read.held, references, targetRoot),
783
811
  unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
784
812
  }));
785
- // Whether a declaration carries the marker decides its code, so one whose marker went
813
+ // Whether a declaration carries the marker decides its code, so one whose answer went
786
814
  // unread is reported by nothing.
787
815
  const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
788
816
  return unread.length === 0
@@ -865,6 +893,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
865
893
  },
866
894
  encapsulated: readManifest(host, targetRoot).declaresExports,
867
895
  edges: readEdgeSides(host, targetRoot),
896
+ filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
868
897
  },
869
898
  selfCheck,
870
899
  intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
@@ -10,7 +10,7 @@ import { isInterfaceDeclaration, isModuleBlock, isModuleDeclaration, } from "@ty
10
10
  import { nodeKey } from "./inventory.js";
11
11
  import { referenceKey } from "./matrix.js";
12
12
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
13
- function isGlobalBlock(symbol, held, files) {
13
+ export function isGlobalBlock(symbol, held, files) {
14
14
  return (symbol.kind === "namespace" &&
15
15
  files.has(symbol.parent) &&
16
16
  symbol.ref.endsWith("#global") &&
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "6.0.0",
6
6
  result: "pass",
7
- digest: "sha256:54d3de663b7867df0d50251bf79c3b3d1f023abd178fb477cf0bcd3a01ceb2e5",
7
+ digest: "sha256:e2a3ba92a5a6fe81174439a944333daf8d17e52fdacb8599fc50e3adb1569773",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * The uses contextual types make of the target's members, which no name node resolves to.
3
3
  * An object-literal property writes the member of its name on each constituent of the
4
- * literal's contextual type that declares one. A value reaching a position typed by another
5
- * object type reads each of its members that type declares, through array elements and
6
- * the parameters and results of functions and constructors too; a class instance there is
7
- * the interface-satisfaction class's evidence instead.
4
+ * literal's contextual type that declares one, and so does a property of a literal whose
5
+ * type is inferred, where a value of that type reaches such a position. A value reaching a
6
+ * position typed by another object type reads each of its members that type declares,
7
+ * through array elements and the parameters and results of functions and constructors
8
+ * too; a class instance there is the interface-satisfaction class's evidence instead.
8
9
  */
9
10
  import { isClassDeclaration, isClassExpression, isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/ast";
10
11
  import { SignatureKind } from "@typescript/native/sync";
@@ -164,6 +165,33 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
164
165
  const members = [...membersOf(part).values()];
165
166
  return members.length > 0 && members.every((member) => !member.own);
166
167
  };
168
+ const literalTypes = new Map();
169
+ /**
170
+ * Whether an object type is an object literal's own, inferred where no contextual type
171
+ * names it: every member a property an object literal of the target writes.
172
+ */
173
+ const literalTyped = (part) => {
174
+ let known = literalTypes.get(part.id);
175
+ if (known === undefined) {
176
+ const members = [...membersOf(part).values()];
177
+ known =
178
+ members.length > 0 &&
179
+ members.every((member) => member.ids.length === 0 &&
180
+ member.declarations.length > 0 &&
181
+ member.declarations.every((handle) => literalProperty(handle) !== undefined));
182
+ literalTypes.set(part.id, known);
183
+ }
184
+ return known;
185
+ };
186
+ /** The name node of the object literal property one declaration handle names, if it is one. */
187
+ const literalProperty = (handle) => {
188
+ const node = project.declarationAt(handle)?.node;
189
+ return node !== undefined &&
190
+ (isPropertyAssignment(node) || isShorthandPropertyAssignment(node)) &&
191
+ isObjectLiteralExpression(node.parent)
192
+ ? node.name
193
+ : undefined;
194
+ };
167
195
  const elements = new Map();
168
196
  /** The element type of an array type, read once per type. */
169
197
  const elementOf = (type) => {
@@ -246,10 +274,13 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
246
274
  const sources = partsOf(source).filter((one) => !targets.some((part) => part.id === one.id));
247
275
  for (const one of sources) {
248
276
  if (carries(one)) {
249
- into.push({ source: one, targets, outside: false });
277
+ into.push({ source: one, targets, sourceKind: "members" });
250
278
  }
251
279
  else if (outside(one)) {
252
- into.push({ source: one, targets, outside: true });
280
+ into.push({ source: one, targets, sourceKind: "outside" });
281
+ }
282
+ else if (literalTyped(one)) {
283
+ into.push({ source: one, targets, sourceKind: "literal" });
253
284
  }
254
285
  const element = elementOf(one);
255
286
  const called = element === undefined ? callsOf(one) : [];
@@ -290,7 +321,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
290
321
  }
291
322
  seen.add(type.id);
292
323
  return partsOf(type).some((part) => {
293
- if (carries(part) || outside(part)) {
324
+ if (carries(part) || outside(part) || literalTyped(part)) {
294
325
  return true;
295
326
  }
296
327
  const element = elementOf(part);
@@ -307,6 +338,8 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
307
338
  batches += batchesOf(sourced.length);
308
339
  const positions = queries.contextualTypes(sourced.map((one) => one.flow.value), cap);
309
340
  const reads = [];
341
+ // A literal property writes a position's members once, however many values carry it there.
342
+ const carriedWrites = new Set();
310
343
  sourced.forEach(({ flow, type }, index) => {
311
344
  const position = positions[index];
312
345
  if (position === undefined || !isAnswered(position)) {
@@ -315,10 +348,24 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
315
348
  const checked = [];
316
349
  compared(type, position, checked, new Set());
317
350
  const ids = new Set();
318
- for (const { source, targets, outside: held } of checked) {
351
+ for (const { source, targets, sourceKind } of checked) {
319
352
  const tables = targets.map(membersOf);
320
353
  const sourceMembers = membersOf(source);
321
- if (held) {
354
+ if (sourceKind === "literal") {
355
+ for (const [name, member] of sourceMembers) {
356
+ const written = [...new Set(tables.flatMap((table) => table.get(name)?.ids ?? []))];
357
+ for (const at of written.length === 0 ? [] : member.declarations) {
358
+ const node = literalProperty(at);
359
+ const key = `${at.path}#${String(at.index)}>${written.join(",")}`;
360
+ if (node !== undefined && !carriedWrites.has(key)) {
361
+ carriedWrites.add(key);
362
+ writes.push({ at: node, from: flow.from, ids: written });
363
+ }
364
+ }
365
+ }
366
+ continue;
367
+ }
368
+ if (sourceKind === "outside") {
322
369
  for (const table of tables) {
323
370
  for (const [name, member] of table) {
324
371
  if (sourceMembers.has(name)) {
@@ -14,7 +14,7 @@ import { scriptTokens } from "./manifest.js";
14
14
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
15
15
  import { UNANSWERED } from "./query.js";
16
16
  import { sourceFilesOf } from "./source-files.js";
17
- import { workflowTokens } from "./workflow-steps.js";
17
+ import { workflowCommands, workflowTokens } from "./workflow-steps.js";
18
18
  const MANIFEST = "package.json";
19
19
  const MODULES_DIR = "node_modules";
20
20
  const MODULES_SEGMENT = `/${MODULES_DIR}/`;
@@ -697,20 +697,27 @@ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
697
697
  /** The commands one script string runs, split at the shell's list and pipeline operators. */
698
698
  const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
699
699
  /**
700
- * The packages one manifest's scripts preload by a runtime flag's bare module specifier,
701
- * a flag counting only in a command whose program passes it to the runtime, and for Node
702
- * only before the script it runs, whose own arguments come after it.
700
+ * The command strings one manifest runs: each of its `scripts`, and the `exec` command of
701
+ * nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
702
+ * it, which nodemon runs the watched script with:
703
+ * https://github.com/remy/nodemon#config-files
703
704
  */
704
- function scriptPreloadsOf(manifest) {
705
+ function manifestCommands(host, dir, manifest) {
705
706
  const scripts = manifest["scripts"];
707
+ const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
708
+ return [
709
+ ...(isRecord(scripts) ? Object.values(scripts) : []),
710
+ ...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
711
+ ].filter((command) => typeof command === "string");
712
+ }
713
+ /**
714
+ * The packages `scripts` preload by a runtime flag's bare module specifier, a flag
715
+ * counting only in a command whose program passes it to the runtime, and for Node only
716
+ * before the script it runs, whose own arguments come after it.
717
+ */
718
+ function preloadsOf(scripts) {
706
719
  const found = new Set();
707
- if (!isRecord(scripts)) {
708
- return found;
709
- }
710
- for (const script of Object.values(scripts)) {
711
- if (typeof script !== "string") {
712
- continue;
713
- }
720
+ for (const script of scripts) {
714
721
  for (const command of script.split(COMMAND_SEPARATORS)) {
715
722
  const tokens = scriptTokens(command);
716
723
  let at = 0;
@@ -820,6 +827,7 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
820
827
  }
821
828
  }
822
829
  const workflow = new Set(workflowTokens(host, targetRoot));
830
+ const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
823
831
  const declared = [];
824
832
  const ran = new Set();
825
833
  const peers = new Map();
@@ -830,14 +838,14 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
830
838
  continue;
831
839
  }
832
840
  const tokens = scriptTokensOf(read.value);
833
- const preloads = scriptPreloadsOf(read.value);
841
+ const preloads = preloadsOf(manifestCommands(host, dir, read.value));
834
842
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
835
843
  const found = installedOf(host, dir, dependency.name);
836
844
  if (found === undefined && uninstalledHere.has(dir)) {
837
845
  continue;
838
846
  }
839
847
  declared.push(dependency);
840
- if (preloads.has(dependency.name)) {
848
+ if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
841
849
  ran.add(declarationKey(dependency));
842
850
  }
843
851
  if (found === undefined) {
@@ -1,6 +1,6 @@
1
1
  import { DiagnosticCategory } from "@typescript/native/sync";
2
2
  import { baseUrlPaths } from "./base-url.js";
3
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
3
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
4
4
  import { SetupError } from "./setup-failure.js";
5
5
  import { writtenPath } from "./workspace-programs.js";
6
6
  /** Directory names discovery does not descend into. */
@@ -224,47 +224,6 @@ export function consumerProjects(engine, host, root) {
224
224
  function derivedId(targetRoot, configFile) {
225
225
  return relativePath(targetRoot, configFile) ?? configFile;
226
226
  }
227
- /**
228
- * The code of a configuration whose `extends` chain returns to a configuration it already
229
- * passed. The parse reports it and returns, and a snapshot opened from it never answers:
230
- * https://github.com/microsoft/TypeScript/issues/64754
231
- */
232
- const CIRCULAR_EXTENDS = 18000;
233
- /** The configuration files one parsed configuration's own `extends` names by path. */
234
- function extendedPaths(parsed, configFile) {
235
- const { raw } = parsed;
236
- const named = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
237
- return (Array.isArray(named) ? named : [named])
238
- .filter((one) => typeof one === "string")
239
- .filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
240
- .map((one) => resolvePath(dirnamePath(configFile), one));
241
- }
242
- /**
243
- * The `extends` chain from `configFile` to the first configuration it reaches twice, each
244
- * link by path, or undefined where no chain written by path returns.
245
- */
246
- function extendsCycle(engine, configFile, parsed) {
247
- const walk = (chain, at) => {
248
- for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
249
- if (chain.includes(next)) {
250
- return [...chain, next];
251
- }
252
- let read;
253
- try {
254
- read = engine.parseConfigFile(next);
255
- }
256
- catch {
257
- continue;
258
- }
259
- const cycle = walk([...chain, next], read);
260
- if (cycle !== undefined) {
261
- return cycle;
262
- }
263
- }
264
- return undefined;
265
- };
266
- return walk([configFile], parsed);
267
- }
268
227
  /** The configuration files one parsed configuration's `references` name. */
269
228
  function referencesOf(parsed) {
270
229
  // The parse answers a directory reference as the directory; the compiler reads a
@@ -272,53 +231,26 @@ function referencesOf(parsed) {
272
231
  // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
273
232
  return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
274
233
  }
275
- /** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
276
- function circularity(engine, configFile, parsed) {
277
- const chain = extendsCycle(engine, configFile, parsed);
278
- if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
279
- return undefined;
280
- }
281
- return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
282
- }
283
234
  /**
284
- * Why a configuration the `references` reach, transitively, cannot be opened, where one's
285
- * `extends` chain is circular: the compiler opens every referenced project with the one
286
- * that references it.
235
+ * The code of a configuration whose `extends` chain returns to a configuration it already
236
+ * passed. A snapshot that opens two configurations whose `extends` chains reach one such
237
+ * cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
238
+ * so the analysis refuses one before any snapshot is asked to open it.
287
239
  */
288
- function referencedCircularity(engine, references, visited) {
289
- for (const reference of references) {
290
- if (visited.has(reference)) {
291
- continue;
292
- }
293
- visited.add(reference);
294
- let parsed;
295
- try {
296
- parsed = engine.parseConfigFile(reference);
297
- }
298
- catch {
299
- continue;
300
- }
301
- const reason = circularity(engine, reference, parsed) ??
302
- referencedCircularity(engine, referencesOf(parsed), visited);
303
- if (reason !== undefined) {
304
- return reason;
305
- }
306
- }
307
- return undefined;
308
- }
240
+ const CIRCULAR_EXTENDS = 18000;
309
241
  /**
310
- * The configuration files one configuration's `references` name, or why the compiler
311
- * cannot read the configuration: it throws, or its `extends` chain or that of a
312
- * configuration it references is circular, which the analysis refuses before any
313
- * snapshot is asked to open it.
242
+ * The configurations one configuration file's `references` name, or why it cannot be read:
243
+ * the compiler throws, or its parse reports the `extends` chain circular.
314
244
  */
315
245
  function readConfiguration(engine, configFile) {
316
246
  try {
317
247
  const parsed = engine.parseConfigFile(configFile);
318
- const references = referencesOf(parsed);
319
- const reason = circularity(engine, configFile, parsed) ??
320
- referencedCircularity(engine, references, new Set([configFile]));
321
- return reason === undefined ? { references } : { reason };
248
+ const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
249
+ // The compiler leaves the message's `{0}` unfilled:
250
+ // https://github.com/microsoft/TypeScript/pull/64755
251
+ return circular === undefined
252
+ ? { references: referencesOf(parsed) }
253
+ : { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
322
254
  }
323
255
  catch (error) {
324
256
  return { reason: error instanceof Error ? error.message : String(error) };
@@ -106,11 +106,19 @@ export function evidenceRecords(input, detectors, disabled) {
106
106
  site: evidence.site,
107
107
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
108
108
  ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
109
+ ...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
109
110
  });
110
111
  }
111
112
  }
112
113
  return found;
113
114
  }
115
+ /**
116
+ * The records that hold given the declarations a write the mode counts stores into: one
117
+ * that holds only while written holds where its declaration is among them.
118
+ */
119
+ export function writtenRecords(records, written) {
120
+ return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
121
+ }
114
122
  /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
115
123
  export function heldRecords(records, holding) {
116
124
  const found = [];
@@ -87,10 +87,12 @@ function testedByIn(input) {
87
87
  * published API, a public member of a published type included, is `certain` where the
88
88
  * run loaded every consumer the scope declared and declared one, `probable` where one did
89
89
  * not load, and `possible` where it declared none, whatever the configuration says of the
90
- * consumer set. A member an `in` expression tests by name is `possible`.
90
+ * consumer set. A member an `in` expression tests by name, and one a function outside the
91
+ * analysis fills, is `possible`.
91
92
  */
92
93
  function reachabilityClasses(input) {
93
94
  const tested = testedByIn(input);
95
+ const filled = input.boundary.filledOutside ?? new Set();
94
96
  const published = publishedIds(input.swept.matrix.union);
95
97
  const { declared } = input.boundary.consumers;
96
98
  const outside = declared.length === 0
@@ -99,7 +101,7 @@ function reachabilityClasses(input) {
99
101
  ? "certain"
100
102
  : "probable";
101
103
  return (id) => {
102
- if (tested.has(id)) {
104
+ if (tested.has(id) || filled.has(id)) {
103
105
  return "possible";
104
106
  }
105
107
  return published.has(id) ? outside : "certain";
package/dist/src/kinds.js CHANGED
@@ -153,7 +153,7 @@ const ROWS = [
153
153
  code: "DS1301",
154
154
  name: "write-only-symbol",
155
155
  languages: ["go", "ts"],
156
- rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
156
+ rule: "A package-level or module-level variable, struct field, class member, type member or collection that production code writes and never reads. In production mode a read from a test file counts as no read. A write from a loaded consumer's code counts as a read, because the deletion the finding names would break a consumer the target does not own. A write in a consumer's test file counts as a read in the run that counts test references. It counts in a production sweep only where the configuration counts consumer tests as production. In Go a read counts only from a declaration that is live under the finding's liveness relation, which is reference counting: a read from a declaration nothing references, whose own finding has the reachability class `certain`, is no read, so a declaration that only such declarations read is reported where a declaration something references writes it. A comparison of struct values reads every field of the type: in Go, a value of a struct type used as an operand of `==` or `!=`, as a map key, or as a `switch` tag or `case` expression is a read of every field of that type, transitively through the fields of every struct type it holds, recorded at the comparison, because equality reads every field and a field that decides equality carries information. In Go a type argument the program instantiates a type parameter with, where the parameter's constraint is `comparable` or embeds it, is compared as an operand of `==` of that type is, recorded at the instantiation, so the struct values `slices.Equal`, `slices.Index` or `maps.Equal` compares have every field read. `reflect.DeepEqual` is not a comparison under this rule. In Go, a conversion the compiler accepts between two struct types, or between two pointer types that are not named types and whose base types are struct types, reads every field of the two struct types where their struct type literals are distinct, recorded at the conversion, because the conversion is legal only while their fields are identical, ignoring tags, and so no field of either can be deleted alone. It reads in the same way every field of each pair of distinct struct type literals the identity of the two types compares in turn: through each field's type, and from there through the element, key, base, parameter and result types of the type literals it reaches, through the method signatures of an interface type literal, and through the type arguments of an instantiated generic type. A field reached there has no reference of its own, as grammar/symbol-ref.md states, so it is never a subject and those reads hold no finding back. It stops at a defined type, whose fields the conversion does not read, because a defined type is identical to itself whatever fields it declares. A pair whose two sides are one struct type literal, the two struct types themselves included, as where one type is declared from the other or the conversion is to the operand's own type, reads nothing, because a field deleted from that literal leaves both sides identical. Where the operand's type or the conversion's type is a type parameter, the struct type its constraint's core type is takes that side's place, and a type parameter operand reads in the same way the fields of each struct type argument an instantiation the program holds gives it, an instantiation made through another generic declaration's own type parameters included. A conversion to any other type, an interface type among them, reads no field under this rule. In TypeScript a comparison of two object references reads no member, because `===` compares identity and never a member. A TypeScript parameter property, a constructor parameter that carries a modifier, declares a member of its class that the constructor writes with its argument, so the member is judged under this rule, and under every rule that reads a class member, as a member the class body declares is. In TypeScript a property of an object literal writes the member of the same name that the literal's contextual type declares, whether or not the compiler checks the literal for excess properties. Where that contextual type is a union or an intersection, the property writes the member of that name on each constituent that declares one. So a literal that a declaration annotates, a function returns, a call passes or a `satisfies` expression checks writes the members of the type it is given. A literal whose type the compiler infers, as one a callback returns to a generic call, writes in the same way the member of each property's name that each object type a value of the literal's type reaches declares, through array elements, type arguments and the parameters and results of functions, so `ids.map((id) => ({ id }))` returned as `Row[]` writes `Row.id`. An object literal handed to a callee the target does not implement has its members read, as grammar/analysis.md states. A store through the value a field or a member holds reads that field or member rather than writing it, because the store lands in storage every other holder of the value reads. In Go that store is an assignment, an increment, a compound assignment or a `delete` whose target is reached through a struct field holding a map, a slice or a pointer. Examples are `s.f[k] = v`, `s.f.x = v` and `delete(s.f, k)`. The same store through a field holding an array or a struct is a store into the field itself. TypeScript holds every object by reference, so an element or property store through a member's value reads the member. The finding names each write position, so the deletion set is visible. Deleting the subject and its writes also deletes every declaration whose only uses they hold. Such a declaration is a type parameter of the subject's declaring type that the subject's type alone names, an import binding the writes alone use, or a declaration whose every use is in the type annotation of the subject or of another subject this rule reports, or in the writes those findings name. The finding's message names each such declaration, and no finding is reported about the declaration itself.",
157
157
  defaultSeverity: "deny",
158
158
  maxClass: "certain",
159
159
  fixability: "deletable",
package/dist/src/query.js CHANGED
@@ -220,6 +220,7 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
220
220
  symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
221
221
  signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
222
222
  resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
223
+ resolvedSignatures: (nodes, cap = DEFAULT_BATCH_CAP) => each("getResolvedSignature", nodes, cap, nodeLocation, (node) => checker.getResolvedSignature.gen(node), (node) => checker.getResolvedSignature(node)),
223
224
  parameterType: (signature, index) => one("getParameterType", () => `signature ${String(signature.id)} parameter ${String(index)}`, () => checker.getParameterType(signature, index)),
224
225
  genericOf: (signature) => signature.target === undefined
225
226
  ? undefined
@@ -1,4 +1,3 @@
1
- import { fails } from "../config.js";
2
1
  import { coalesced } from "../json-chunks.js";
3
2
  import { KINDS } from "../kinds.js";
4
3
  /** The escapes a workflow command's message needs, so a line break does not end the command. */
@@ -19,6 +18,12 @@ function escapeProperty(value) {
19
18
  function kindName(code) {
20
19
  return KINDS.get(code)?.name ?? code;
21
20
  }
21
+ /** The command each finding severity is annotated with, whatever the failing severity. */
22
+ const LEVELS = {
23
+ deny: "error",
24
+ warn: "warning",
25
+ allow: "notice",
26
+ };
22
27
  function command(level, at, endLine, title, message) {
23
28
  return `::${level} file=${escapeProperty(at.path)},line=${String(at.line)},col=${String(at.column)},endLine=${String(endLine)},title=${escapeProperty(title)}::${escapeData(message)}`;
24
29
  }
@@ -26,16 +31,15 @@ function command(level, at, endLine, title, message) {
26
31
  const SKIP_TITLE = "type error skipped";
27
32
  const SKIP_MESSAGE = "the analysis did not evaluate the function or statement holding this type error: ";
28
33
  /**
29
- * One workflow annotation per finding, then one per stale suppression, each naming its
30
- * file, line, column and last line, titled by its code and kind, then one warning per
31
- * type-error skip naming its file and line. A finding at or above the failing severity is
32
- * an error and one below it a warning; a stale suppression is an error, because its kind
33
- * is fixed on at the failing severity.
34
+ * The annotations `contract/grammar/annotations.md` states: one per finding, at the
35
+ * command its severity names, and one error per stale suppression, each naming its file,
36
+ * line, column and last line and titled by its code and kind; then one warning per
37
+ * type-error skip naming its file and line, and one per note naming its directory.
34
38
  */
35
- export function annotations(report, options) {
39
+ export function annotations(report) {
36
40
  return coalesced((function* lines() {
37
41
  for (const found of report.findings) {
38
- yield `${command(fails(found.severity, options.failOn) ? "error" : "warning", found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
42
+ yield `${command(LEVELS[found.severity], found.position, found.position.end_line, `${found.code} ${found.kind}`, found.message)}\n`;
39
43
  }
40
44
  for (const stale of report.stale_suppressions) {
41
45
  yield `${command("error", stale.position, stale.position.line, `${stale.code} ${kindName(stale.code)}`, stale.message)}\n`;
@@ -43,5 +47,8 @@ export function annotations(report, options) {
43
47
  for (const skip of report.type_error_skips) {
44
48
  yield `::warning file=${escapeProperty(skip.path)},line=${String(skip.line)},title=${escapeProperty(SKIP_TITLE)}::${escapeData(SKIP_MESSAGE + skip.message)}\n`;
45
49
  }
50
+ for (const note of report.notes) {
51
+ yield `::warning file=${escapeProperty(note.path)},title=${escapeProperty(note.kind)}::${escapeData(note.message)}\n`;
52
+ }
46
53
  })());
47
54
  }