@cplieger/deadset-ts 5.0.0 → 5.1.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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. package/src/version.ts +1 -1
@@ -1,12 +1,15 @@
1
- import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
1
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isDecorator, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExpressionStatement, isExternalModuleReference, isForInStatement, isForOfStatement, isForStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVoidExpression, SyntaxKind, } from "@typescript/native/unstable/ast";
2
2
  import { aliasChains } from "./alias-chain.js";
3
+ import { contextualUses } from "./contextual-uses.js";
3
4
  import { destructuring, propertyReadsOf } from "./destructuring.js";
4
5
  import { globExpression } from "./glob.js";
5
6
  import { declarationsByName, nodeKey } from "./inventory.js";
6
- import { relativePath } from "./paths.js";
7
+ import { overrideReferences } from "./overrides.js";
8
+ import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.js";
7
9
  import { byPosition, isComponentFile, renderPosition } from "./position.js";
8
- import { DEFAULT_BATCH_CAP, UNANSWERED } from "./query.js";
10
+ import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
9
11
  import { typeQueryAliases } from "./type-query-alias.js";
12
+ import { handedOn, valuesOf } from "./value-flow.js";
10
13
  /**
11
14
  * The operators whose left side one assignment stores into. A compound assignment is
12
15
  * among them: where its value is discarded it reads its target only to write the result
@@ -94,7 +97,11 @@ function markWrite(target, writes) {
94
97
  return;
95
98
  }
96
99
  if (isElementAccessExpression(held)) {
97
- markWrite(held.expression, writes);
100
+ // A store through a member's value lands in storage every holder of the value
101
+ // reads, so it reads the member; a variable only ever stored into stays written.
102
+ if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
103
+ markWrite(held.expression, writes);
104
+ }
98
105
  return;
99
106
  }
100
107
  if (isArrayLiteralExpression(held)) {
@@ -194,6 +201,19 @@ function evaluatedBy(node) {
194
201
  }
195
202
  return undefined;
196
203
  }
204
+ /**
205
+ * The own file a module specifier with a query evaluates ({@link queriedModulePath}): the
206
+ * checker resolves the whole specifier to whatever module declaration matches it.
207
+ */
208
+ function queriedFile(file, specifier, fileIdByName) {
209
+ if (!isStringLiteral(specifier) && !isNoSubstitutionTemplateLiteral(specifier)) {
210
+ return undefined;
211
+ }
212
+ const path = queriedModulePath(specifier.text);
213
+ return path === undefined
214
+ ? undefined
215
+ : fileIdByName.get(resolvePath(dirnamePath(file.fileName), path));
216
+ }
197
217
  /**
198
218
  * The re-exports one node writes, if it is an export declaration. A specifier and a
199
219
  * namespace export are declarations whose names are where they are written, so no use is
@@ -232,6 +252,27 @@ function linksOf(file, node, declarations, enclosing) {
232
252
  }
233
253
  return found;
234
254
  }
255
+ /**
256
+ * Whether one object literal is a destructuring target, written on the left of an
257
+ * assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
258
+ */
259
+ function isAssignmentPattern(node) {
260
+ let child = node;
261
+ let parent = node.parent;
262
+ while (isParenthesizedExpression(parent) ||
263
+ isArrayLiteralExpression(parent) ||
264
+ isObjectLiteralExpression(parent) ||
265
+ isSpreadElement(parent) ||
266
+ isSpreadAssignment(parent) ||
267
+ (isPropertyAssignment(parent) && parent.initializer === child)) {
268
+ child = parent;
269
+ parent = parent.parent;
270
+ }
271
+ if (isBinaryExpression(parent)) {
272
+ return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
273
+ }
274
+ return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
275
+ }
235
276
  /**
236
277
  * Every name node one file holds that is a use rather than a declaration, in source
237
278
  * order, each with its enclosing declaration and its use, and every re-export the file
@@ -246,6 +287,8 @@ function sitesOf(file, declarations, fileId, unread) {
246
287
  const links = [];
247
288
  const evaluations = [];
248
289
  const decorated = [];
290
+ const literals = [];
291
+ const flows = [];
249
292
  const writes = new Set();
250
293
  const alsoRead = new Set();
251
294
  const declared = new Set();
@@ -286,6 +329,13 @@ function sitesOf(file, declarations, fileId, unread) {
286
329
  }
287
330
  markNames(node);
288
331
  markStores(node, writes, alsoRead);
332
+ if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
333
+ literals.push({ literal: node, from: enclosing });
334
+ }
335
+ // An assertion needs only comparability, so no member of its operand is read by it.
336
+ for (const written of isAsExpression(node) ? [] : handedOn(node)) {
337
+ flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
338
+ }
289
339
  for (const read of propertyReadsOf(node)) {
290
340
  reads.push({ ...read, from: enclosing });
291
341
  }
@@ -308,8 +358,16 @@ function sitesOf(file, declarations, fileId, unread) {
308
358
  enclosing = outer;
309
359
  };
310
360
  file.forEachChild(visit);
311
- return { uses: found, reads, links, evaluations, decorated };
361
+ return { uses: found, literals, flows, reads, links, evaluations, decorated };
312
362
  }
363
+ /** The declaration kinds of a property an object literal declares. */
364
+ const LITERAL_PROPERTY_KINDS = new Set([
365
+ SyntaxKind.PropertyAssignment,
366
+ SyntaxKind.ShorthandPropertyAssignment,
367
+ SyntaxKind.MethodDeclaration,
368
+ SyntaxKind.GetAccessor,
369
+ SyntaxKind.SetAccessor,
370
+ ]);
313
371
  /** The rule a file the analysis classified as test-support code is counted under. */
314
372
  const TEST_SUPPORT_RULE = "test-support";
315
373
  /**
@@ -412,6 +470,59 @@ function importBindings(file) {
412
470
  }
413
471
  return found;
414
472
  }
473
+ /** The value one assignment stores through the name it writes, where it is a plain assignment. */
474
+ function storedValue(name) {
475
+ let target = name;
476
+ if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
477
+ target = name.parent;
478
+ }
479
+ while (isParenthesizedExpression(target.parent)) {
480
+ target = target.parent;
481
+ }
482
+ const parent = target.parent;
483
+ return isBinaryExpression(parent) &&
484
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
485
+ parent.left === target
486
+ ? parent.right
487
+ : undefined;
488
+ }
489
+ /**
490
+ * The import bindings of one file that only the values written into one declaration
491
+ * use, for each write into it. A binding is named by an identifier that spells its
492
+ * local name and is not the member a property access names.
493
+ */
494
+ function aloneIn(file, uses, writes, found) {
495
+ const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
496
+ if (bindings.size === 0) {
497
+ return [];
498
+ }
499
+ const occurrences = new Map();
500
+ for (const site of uses) {
501
+ const text = site.node.getText();
502
+ const parent = site.node.parent;
503
+ if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
504
+ continue;
505
+ }
506
+ if (!occurrences.get(text)?.includes(site.node)) {
507
+ occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
508
+ }
509
+ }
510
+ const inside = (node, value) => node.pos >= value.pos && node.end <= value.end;
511
+ const result = [];
512
+ for (const write of writes) {
513
+ const to = found[write.index]?.to;
514
+ const sameTarget = writes.filter((one) => found[one.index]?.to === to);
515
+ const names = [...occurrences]
516
+ .filter(([, nodes]) => nodes.some((node) => inside(node, write.value)) &&
517
+ nodes.every((node) => sameTarget.some((one) => inside(node, one.value))))
518
+ .map(([name]) => name)
519
+ .sort(compare);
520
+ if (names.length > 0) {
521
+ result.push({ index: write.index, names });
522
+ }
523
+ }
524
+ return result;
525
+ }
415
526
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
416
527
  function compare(a, b) {
417
528
  if (a === b) {
@@ -442,7 +553,8 @@ export function references(project, held, targetRoot, options) {
442
553
  const found = [];
443
554
  const tests = [];
444
555
  const chains = aliasChains(project, held);
445
- const destructured = destructuring(project, cap);
556
+ const tables = propertyTables(project.queries);
557
+ const destructured = destructuring(project, cap, tables);
446
558
  let batched = 0;
447
559
  let fileBatches = 0;
448
560
  let residueFallbacks = 0;
@@ -455,6 +567,7 @@ export function references(project, held, targetRoot, options) {
455
567
  .ownSourceFiles()
456
568
  .filter((file) => relativePath(consumer.root, file.fileName) !== undefined &&
457
569
  relativePath(targetRoot, file.fileName) === undefined);
570
+ const ownPaths = project.ownPaths();
458
571
  const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
459
572
  let topLevel;
460
573
  /** The declarations one file's top level holds. */
@@ -551,14 +664,90 @@ export function references(project, held, targetRoot, options) {
551
664
  residueFallbacks += 1;
552
665
  return project.resolvedSymbolAt(project.handle(site.node));
553
666
  };
554
- for (const file of files) {
667
+ const walked = files.map((file) => {
668
+ const fileId = declaredAt(file) ?? "";
669
+ return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
670
+ });
671
+ const fileIdByName = new Map(walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]));
672
+ const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
673
+ const writtenBy = new Map();
674
+ for (const write of contextual.writes) {
675
+ const property = write.at.parent;
676
+ writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
677
+ }
678
+ const forwardedCache = new Map();
679
+ /**
680
+ * The members a literal property one symbol declares writes: a use of the property
681
+ * reaches them, because the literal's type carries the data the member describes.
682
+ */
683
+ const forwarded = (symbol) => {
684
+ const known = forwardedCache.get(symbol.id);
685
+ if (known !== undefined) {
686
+ return known;
687
+ }
688
+ const found = [];
689
+ if (writtenBy.size > 0) {
690
+ for (const handle of symbol.declarations) {
691
+ if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
692
+ continue;
693
+ }
694
+ const node = project.declarationAt(handle)?.node;
695
+ const ids = node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
696
+ for (const id of ids ?? []) {
697
+ if (!found.some((target) => target.id === id)) {
698
+ found.push({ id, stepped: true, guessed: false });
699
+ }
700
+ }
701
+ }
702
+ }
703
+ forwardedCache.set(symbol.id, found);
704
+ return found;
705
+ };
706
+ /** One resolved symbol's declarations along its chain, and the members its literal properties write. */
707
+ const targetsOf = (symbol) => {
708
+ const through = forwarded(symbol);
709
+ return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
710
+ };
711
+ const testFile = new Map();
712
+ for (const { file } of walked) {
555
713
  const path = renderPosition(file, root, 0).path;
556
714
  const test = patterns.some((pattern) => pattern.test(path));
715
+ testFile.set(file, test);
557
716
  if (test) {
558
717
  tests.push(path);
559
718
  }
560
- const fileId = declaredAt(file) ?? "";
561
- const { uses, reads, links, evaluations, decorated } = sitesOf(file, declarations, fileId, unread);
719
+ }
720
+ /** Each write reference's index in `found`, by its file, with the value it stores. */
721
+ const valued = new Map();
722
+ const holdsValue = (file, value) => {
723
+ if (value !== undefined) {
724
+ valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
725
+ }
726
+ };
727
+ for (const [use, sites] of [
728
+ ["write", contextual.writes],
729
+ ["read", contextual.reads],
730
+ ]) {
731
+ for (const site of sites) {
732
+ const file = site.at.getSourceFile();
733
+ const position = renderPosition(file, root, site.at.getStart());
734
+ for (const id of site.ids) {
735
+ found.push({
736
+ from: site.from,
737
+ to: id,
738
+ position,
739
+ use,
740
+ resolution: "contextual",
741
+ test: testFile.get(file) === true,
742
+ });
743
+ if (use === "write") {
744
+ holdsValue(file, site.at.parent);
745
+ }
746
+ }
747
+ }
748
+ }
749
+ for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
750
+ const test = testFile.get(file) === true;
562
751
  // A component file's markup may use any binding its blocks declare at the top level.
563
752
  const markup = isComponentFile(file);
564
753
  const bindings = markup ? importBindings(file) : [];
@@ -622,6 +811,17 @@ export function references(project, held, targetRoot, options) {
622
811
  }
623
812
  }
624
813
  for (const evaluation of evaluations) {
814
+ const queried = queriedFile(file, evaluation.node, fileIdByName);
815
+ if (queried !== undefined) {
816
+ found.push({
817
+ from: evaluation.from,
818
+ to: queried,
819
+ position: renderPosition(file, root, evaluation.node.getStart()),
820
+ use: "evaluation",
821
+ resolution: "syntax",
822
+ test,
823
+ });
824
+ }
625
825
  const module = answered.get(evaluation.node);
626
826
  if (module === undefined) {
627
827
  continue;
@@ -697,7 +897,7 @@ export function references(project, held, targetRoot, options) {
697
897
  stepped: false,
698
898
  guessed: true,
699
899
  }))
700
- : chains.chainOf(symbol);
900
+ : targetsOf(symbol);
701
901
  for (const target of targets) {
702
902
  found.push({
703
903
  from: read.from,
@@ -724,7 +924,7 @@ export function references(project, held, targetRoot, options) {
724
924
  // the use names every link of the chain its name stands at the head of.
725
925
  const targets = throughAliases(symbol === UNANSWERED
726
926
  ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
727
- : chains.chainOf(symbol));
927
+ : targetsOf(symbol));
728
928
  for (const target of targets) {
729
929
  found.push({
730
930
  from: site.from,
@@ -734,9 +934,25 @@ export function references(project, held, targetRoot, options) {
734
934
  resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
735
935
  test,
736
936
  });
937
+ if (site.use === "write") {
938
+ holdsValue(file, storedValue(site.node));
939
+ }
737
940
  }
738
941
  }
739
942
  }
943
+ const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
944
+ for (const [file, writes] of valued) {
945
+ const uses = usesOf.get(file) ?? [];
946
+ for (const { index, names } of aloneIn(file, uses, writes, found)) {
947
+ const reference = found[index];
948
+ if (reference !== undefined) {
949
+ found[index] = { ...reference, alone: names };
950
+ }
951
+ }
952
+ }
953
+ if (consumer === undefined) {
954
+ found.push(...overrideReferences(project, held, chains, new Set(tests)));
955
+ }
740
956
  found.sort((a, b) => byPosition(a.position, b.position) ||
741
957
  compare(a.to, b.to) ||
742
958
  compare(a.from, b.from) ||
@@ -755,6 +971,8 @@ export function references(project, held, targetRoot, options) {
755
971
  aliasSteps: chains.steps,
756
972
  patternBatches: destructured.cost.patternBatches,
757
973
  patternLookups: destructured.cost.patternLookups,
974
+ contextualBatches: contextual.batches,
975
+ contextualLookups: contextual.lookups,
758
976
  },
759
977
  };
760
978
  }
@@ -133,18 +133,19 @@ function readStrings(parent, name, path, label, minItems, allowed) {
133
133
  throw malformed(label, path, `holds ${String(value.length)} entries, want at least ${String(minItems)}`);
134
134
  }
135
135
  const seen = new Set();
136
- for (const entry of value) {
136
+ for (const [index, entry] of value.entries()) {
137
+ const at = `${path}[${String(index)}]`;
137
138
  if (typeof entry !== "string") {
138
- throw malformed(label, path, "holds an entry that is not a string");
139
+ throw malformed(label, at, "is not a string");
139
140
  }
140
141
  if (entry === "") {
141
- throw malformed(label, path, "holds an empty entry");
142
+ throw malformed(label, at, "is empty");
142
143
  }
143
144
  if (seen.has(entry)) {
144
- throw malformed(label, path, `names ${JSON.stringify(entry)} twice`);
145
+ throw malformed(label, at, `names ${JSON.stringify(entry)} again`);
145
146
  }
146
147
  if (allowed !== undefined && !allowed.includes(entry)) {
147
- throw malformed(label, path, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
148
+ throw malformed(label, at, `${JSON.stringify(entry)} is not one of ${spell(allowed)}`);
148
149
  }
149
150
  seen.add(entry);
150
151
  }
@@ -154,9 +155,9 @@ function readStrings(parent, name, path, label, minItems, allowed) {
154
155
  const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
155
156
  function readComponentExtensions(ts, label) {
156
157
  const extensions = readStrings(ts, "component_extensions", "ts.component_extensions", label, 0);
157
- const wrong = extensions?.find((one) => !COMPONENT_EXTENSION.test(one));
158
- if (wrong !== undefined) {
159
- throw malformed(label, "ts.component_extensions", `${JSON.stringify(wrong)} is not a full stop followed by letters and digits`);
158
+ const wrong = extensions?.findIndex((one) => !COMPONENT_EXTENSION.test(one)) ?? -1;
159
+ if (extensions !== undefined && wrong !== -1) {
160
+ throw malformed(label, `ts.component_extensions[${String(wrong)}]`, `${JSON.stringify(extensions[wrong])} is not a full stop followed by letters and digits`);
160
161
  }
161
162
  return extensions;
162
163
  }
@@ -164,9 +165,9 @@ function readComponentExtensions(ts, label) {
164
165
  const CONVENTION_NAMES = new Set(CONVENTION_ROWS.map((row) => row.name));
165
166
  function readDisabledConventions(ts, label) {
166
167
  const names = readStrings(ts, "disabled_conventions", "ts.disabled_conventions", label, 0);
167
- const unknown = names?.find((one) => !CONVENTION_NAMES.has(one));
168
- if (unknown !== undefined) {
169
- throw malformed(label, "ts.disabled_conventions", `${JSON.stringify(unknown)} names no convention row this analyzer carries`);
168
+ const unknown = names?.findIndex((one) => !CONVENTION_NAMES.has(one)) ?? -1;
169
+ if (names !== undefined && unknown !== -1) {
170
+ throw malformed(label, `ts.disabled_conventions[${String(unknown)}]`, `${JSON.stringify(names[unknown])} names no convention row this analyzer carries`);
170
171
  }
171
172
  return names;
172
173
  }
package/dist/src/roots.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * decides; the published API only supposes a consumer, so a published export is live
8
8
  * under reachability and still a candidate under reference counting.
9
9
  */
10
+ import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
10
11
  import { aliasChains } from "./alias-chain.js";
11
12
  import { conventionEntries } from "./conventions.js";
12
13
  import { entryPoints } from "./entry-points.js";
@@ -131,7 +132,7 @@ export function roots(project, held, targetRoot, options) {
131
132
  publishes: entry.published &&
132
133
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
133
134
  })),
134
- ...(options.workspaceEntries ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
+ ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
135
136
  ];
136
137
  for (const { entry, publishes } of entries) {
137
138
  if (options.publishedAPI && publishes) {
@@ -158,7 +159,7 @@ export function roots(project, held, targetRoot, options) {
158
159
  : conventionEntries(options.conventions, files)) {
159
160
  enter(file, "convention", row, true);
160
161
  }
161
- for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot)) {
162
+ for (const point of entryPoints(project, files, options.testFiles, options.host, targetRoot, options.parse)) {
162
163
  enter(point.file, point.rule, point.source, point.exports);
163
164
  }
164
165
  for (const file of files.byPath.values()) {
@@ -188,10 +189,11 @@ export function roots(project, held, targetRoot, options) {
188
189
  }
189
190
  }
190
191
  }
191
- if (options.publishedAPI) {
192
- for (const id of publishedAPI(published, exportsOf, held)) {
193
- add(id, "published-api", "");
194
- }
192
+ const api = options.publishedAPI
193
+ ? publishedAPI(project, published, exportsOf, held, files.byPath)
194
+ : { ids: [], guessed: [] };
195
+ for (const id of api.ids) {
196
+ add(id, "published-api", "");
195
197
  }
196
198
  const unmatched = configured(held, options.patterns, add);
197
199
  const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
@@ -205,7 +207,7 @@ export function roots(project, held, targetRoot, options) {
205
207
  configFile: project.configFile,
206
208
  liveUnderReachability: ordered,
207
209
  unmatched,
208
- unanswered: answered ? [] : held.symbols.map((symbol) => symbol.id),
210
+ unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
209
211
  };
210
212
  }
211
213
  /**
@@ -255,52 +257,134 @@ function exportTables(project, held, wanted) {
255
257
  /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
256
258
  const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
257
259
  /**
258
- * Every declaration a consumer of the library can name: what the published files
259
- * export, and below each, the members a consumer reaches through it.
260
- *
261
- * The published files are the ones `exports` reaches where the manifest declares
262
- * one, because a package that declares `exports` has said exactly what a consumer may
263
- * import; where it declares none, they are the ones the members that preceded it
264
- * name. Below a published module or namespace the walk follows what its export table
265
- * names; below any other published declaration it follows every member a consumer
266
- * can write: a public one, and a protected one, which a consumer's subclass names. A
267
- * private member is not published, nor is anything below one, and a type parameter
268
- * is not published either, because a consumer supplies a type argument by position.
269
- *
270
- * A published file is not itself published. A consumer imports a module and names
271
- * what it exports; the file is a root because the manifest names it, which is a
272
- * different reason.
260
+ * Every declaration a consumer of the library can name: what the published files export
261
+ * and, below each, the members a consumer writes: a namespace's exported ones, any other
262
+ * declaration's public and protected ones, never a private one or a type parameter. A
263
+ * declaration a published one's type names is published too, exported or not. A type name
264
+ * the checker leaves unanswered publishes every declaration spelled as it is, and those are
265
+ * `guessed`. A published file is not itself published.
273
266
  */
274
- function publishedAPI(published, exportsOf, held) {
267
+ function publishedAPI(project, published, exportsOf, held, byPath) {
275
268
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
276
269
  const children = childrenOf(held);
270
+ const nodes = declarationNodes(held, byPath);
271
+ const chains = aliasChains(project, held);
277
272
  const reached = new Set();
273
+ const guessed = new Set();
278
274
  const pending = [];
275
+ const reach = (id) => {
276
+ if (!reached.has(id) && byId.get(id)?.kind !== "file") {
277
+ reached.add(id);
278
+ pending.push(id);
279
+ }
280
+ };
279
281
  for (const file of published) {
280
282
  for (const id of exportsOf.get(file.fileName) ?? []) {
281
283
  if (byId.get(id)?.kind === "file") {
282
284
  pending.push(id);
283
285
  }
284
- else if (!reached.has(id)) {
285
- reached.add(id);
286
- pending.push(id);
286
+ else {
287
+ reach(id);
287
288
  }
288
289
  }
289
290
  }
290
- for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
291
- const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
292
- for (const member of children.get(next) ?? []) {
293
- const reachable = exporting
294
- ? member.exported
295
- : member.visibility !== "private" && member.visibility !== "private-name";
296
- if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
297
- continue;
291
+ while (pending.length > 0) {
292
+ const round = [];
293
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
294
+ round.push(next);
295
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
296
+ for (const member of children.get(next) ?? []) {
297
+ const reachable = exporting
298
+ ? member.exported
299
+ : member.visibility !== "private" && member.visibility !== "private-name";
300
+ if (member.kind !== "type-parameter" && reachable) {
301
+ reach(member.id);
302
+ }
298
303
  }
299
- reached.add(member.id);
300
- pending.push(member.id);
301
304
  }
305
+ const named = round.flatMap((id) => {
306
+ const symbol = byId.get(id);
307
+ return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
308
+ });
309
+ project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
310
+ if (symbol === UNANSWERED) {
311
+ for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
312
+ guessed.add(id);
313
+ reach(id);
314
+ }
315
+ }
316
+ else if (symbol !== undefined) {
317
+ for (const target of chains.chainOf(symbol)) {
318
+ reach(target.id);
319
+ }
320
+ }
321
+ });
302
322
  }
303
- return [...reached];
323
+ return { ids: [...reached], guessed: [...guessed] };
324
+ }
325
+ /** The type names declarations of the inventory write, each file walked once, when first asked. */
326
+ function declarationNodes(held, byPath) {
327
+ const walked = new Map();
328
+ const nodesIn = (path) => {
329
+ const known = walked.get(path);
330
+ if (known !== undefined) {
331
+ return known;
332
+ }
333
+ const found = { byId: new Map(), nodes: new Set() };
334
+ const file = byPath.get(path);
335
+ if (file !== undefined) {
336
+ const visit = (node) => {
337
+ const id = held.declarations.get(nodeKey(file, node));
338
+ if (id !== undefined) {
339
+ found.nodes.add(node);
340
+ if (!found.byId.has(id)) {
341
+ found.byId.set(id, node);
342
+ }
343
+ }
344
+ node.forEachChild(visit);
345
+ };
346
+ visit(file);
347
+ }
348
+ walked.set(path, found);
349
+ return found;
350
+ };
351
+ return {
352
+ typeNamesOf: (symbol) => {
353
+ const { byId, nodes } = nodesIn(symbol.position.path);
354
+ const declaration = byId.get(symbol.id);
355
+ return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
356
+ },
357
+ };
358
+ }
359
+ /**
360
+ * The names of types one declaration's own syntax writes: in a type reference, a
361
+ * heritage clause or a type query. A function body is not read, and neither is a
362
+ * declaration nested in this one, which `nested` holds and which is read on its own.
363
+ */
364
+ function typeNamesOf(declaration, nested) {
365
+ const found = [];
366
+ const visit = (node) => {
367
+ if (node !== declaration && nested.has(node)) {
368
+ return;
369
+ }
370
+ if (isTypeReferenceNode(node)) {
371
+ found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
372
+ }
373
+ else if (isExpressionWithTypeArguments(node)) {
374
+ found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
375
+ }
376
+ else if (isTypeQueryNode(node)) {
377
+ found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
378
+ }
379
+ const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
380
+ node.forEachChild((child) => {
381
+ if (child !== body) {
382
+ visit(child);
383
+ }
384
+ });
385
+ };
386
+ visit(declaration);
387
+ return found;
304
388
  }
305
389
  /**
306
390
  * Keeps every declaration the configuration names and returns every string that
@@ -225,10 +225,10 @@ function projectsOf(snapshot, built, written) {
225
225
  * snapshot of one client, and answers in their order. A configuration `programs` describes is
226
226
  * built as that program, or opened from the configuration it writes, instead of as a project.
227
227
  * One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
228
- * under `"record"`. The snapshot is disposed after the last visit and the client released
229
- * after it, whether the visit completed or threw.
228
+ * under `"record"`. `last` runs after the last visit while the client still answers; the
229
+ * snapshot is disposed and the client released after it, whether the visit completed or threw.
230
230
  */
231
- export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
231
+ export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
232
232
  try {
233
233
  const built = [];
234
234
  const params = [];
@@ -262,6 +262,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
262
262
  }
263
263
  projects.push(visit(viewOf(project, configFile, engine, log)));
264
264
  }
265
+ last();
265
266
  return {
266
267
  projects,
267
268
  unopened: missing,
@@ -26,6 +26,9 @@ export function accessorsOf(project, held) {
26
26
  export function storesOf(per, exempt, mode) {
27
27
  return {
28
28
  references: per.flatMap((one) => one.references.filter((reference) => !mode.production || !reference.test)),
29
+ readInTests: new Set(per.flatMap((one) => one.references
30
+ .filter((reference) => reference.test && reference.use !== "write")
31
+ .map((reference) => reference.to))),
29
32
  accessors: new Set(per.flatMap((one) => one.accessors)),
30
33
  exempt: new Set(exempt.map((record) => record.id)),
31
34
  };