@cplieger/deadset-ts 7.0.0-dev.2 → 7.0.0-dev.3

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 (75) hide show
  1. package/dist/src/analysis.js +89 -44
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/components.js +62 -20
  4. package/dist/src/configuration-files.js +5 -5
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +71 -14
  7. package/dist/src/convention-rows.js +6 -2
  8. package/dist/src/conventions.js +3 -1
  9. package/dist/src/dependencies.js +80 -26
  10. package/dist/src/element-access.js +341 -0
  11. package/dist/src/erased-stores.js +317 -0
  12. package/dist/src/findings/completion.js +46 -15
  13. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  14. package/dist/src/findings/emitters.js +1 -1
  15. package/dist/src/findings/self-check.js +17 -4
  16. package/dist/src/findings-pass.js +6 -5
  17. package/dist/src/in-operands.js +30 -0
  18. package/dist/src/indexed-access.js +264 -125
  19. package/dist/src/intra-function-parts.js +25 -11
  20. package/dist/src/inventory.js +25 -0
  21. package/dist/src/keyof-comparisons.js +170 -0
  22. package/dist/src/kinds.js +17 -7
  23. package/dist/src/manifest.js +6 -16
  24. package/dist/src/matrix.js +20 -2
  25. package/dist/src/query.js +3 -0
  26. package/dist/src/references.js +12 -1
  27. package/dist/src/report.js +1 -1
  28. package/dist/src/reporters/sarif.js +4 -0
  29. package/dist/src/serialization-contract.js +43 -14
  30. package/dist/src/session.js +12 -7
  31. package/dist/src/suppress-file.js +58 -6
  32. package/dist/src/suppress-inline.js +13 -4
  33. package/dist/src/suppress.js +54 -24
  34. package/dist/src/unimplemented-callees.js +312 -0
  35. package/dist/src/value-reach.js +7 -7
  36. package/dist/src/verbs/analyze.js +11 -9
  37. package/jsr.json +1 -1
  38. package/package.json +2 -2
  39. package/src/analysis.ts +135 -52
  40. package/src/augmentations.ts +14 -11
  41. package/src/components.ts +103 -21
  42. package/src/configuration-files.ts +5 -4
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contextual-uses.ts +81 -17
  45. package/src/convention-rows.ts +6 -2
  46. package/src/conventions.ts +10 -1
  47. package/src/dependencies.ts +107 -30
  48. package/src/element-access.ts +446 -0
  49. package/src/erased-stores.ts +413 -0
  50. package/src/file-facts.ts +2 -0
  51. package/src/findings/completion.ts +54 -20
  52. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  53. package/src/findings/emitters.ts +1 -1
  54. package/src/findings/self-check.ts +18 -4
  55. package/src/findings-pass.ts +12 -5
  56. package/src/in-operands.ts +45 -0
  57. package/src/indexed-access.ts +303 -145
  58. package/src/intra-function-parts.ts +41 -16
  59. package/src/inventory.ts +32 -0
  60. package/src/keyof-comparisons.ts +211 -0
  61. package/src/kinds.ts +20 -7
  62. package/src/manifest.ts +6 -21
  63. package/src/matrix.ts +34 -2
  64. package/src/query.ts +10 -0
  65. package/src/references.ts +23 -1
  66. package/src/report.ts +3 -1
  67. package/src/reporters/sarif.ts +4 -0
  68. package/src/serialization-contract.ts +53 -14
  69. package/src/session.ts +34 -29
  70. package/src/suppress-file.ts +68 -5
  71. package/src/suppress-inline.ts +14 -4
  72. package/src/suppress.ts +63 -28
  73. package/src/unimplemented-callees.ts +411 -0
  74. package/src/value-reach.ts +11 -7
  75. package/src/verbs/analyze.ts +15 -9
@@ -41,7 +41,8 @@ const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
41
41
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
42
42
  /**
43
43
  * The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
44
- * resolves `setupFiles` against that root (https://vitest.dev/config/setupfiles).
44
+ * resolves `setupFiles` and `globalSetup` against that root
45
+ * (https://vitest.dev/config/setupfiles, https://vitest.dev/config/globalsetup).
45
46
  */
46
47
  const VITEST_ROOTS = ["test.root", "root"];
47
48
  /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
@@ -540,6 +541,9 @@ export const CONVENTION_ROWS = [
540
541
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
541
542
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
542
543
  ],
543
- testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
544
+ testFiles: [
545
+ { property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
546
+ { property: "test.globalSetup", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
547
+ ],
544
548
  },
545
549
  ];
@@ -707,7 +707,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
707
707
  }
708
708
  const version = versions.get(row.package);
709
709
  if (version === undefined) {
710
- if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
710
+ if (missing.has(row.package) ||
711
+ manifest.nested === true ||
712
+ !declares(declared, row.package, REQUIRED_SECTIONS)) {
711
713
  continue;
712
714
  }
713
715
  missing.add(row.package);
@@ -29,11 +29,13 @@ const PROJECT_OWNER = "";
29
29
  /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
30
30
  export function configurationNeeds(packages) {
31
31
  return {
32
- packages: new Set(packages),
33
32
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
33
+ at: new Map([[ANYWHERE, new Set(packages)]]),
34
34
  unanswered: [],
35
35
  };
36
36
  }
37
+ /** The key of {@link ProjectNeeds.at} for a use no file of the target governs. */
38
+ const ANYWHERE = "";
37
39
  function isRecord(value) {
38
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
39
41
  }
@@ -207,9 +209,10 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
207
209
  }
208
210
  const packages = configurationPackages(host, project.configFile, project.program.getCompilerOptions(), programPackages);
209
211
  const uses = new Map();
210
- const use = (owners, named) => {
212
+ const at = new Map();
213
+ const use = (owners, named, path) => {
211
214
  for (const one of named) {
212
- packages.add(one);
215
+ at.set(path, (at.get(path) ?? new Set()).add(one));
213
216
  for (const owner of owners) {
214
217
  let set = uses.get(owner);
215
218
  if (set === undefined) {
@@ -220,10 +223,13 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
220
223
  }
221
224
  }
222
225
  };
223
- use([PROJECT_OWNER], packages);
226
+ use([PROJECT_OWNER], packages, project.configFile);
224
227
  const unanswered = [];
225
228
  for (const file of project.ownSourceFiles()) {
226
- if (fileNeeds(project, held, file, programPackages, use)) {
229
+ const inFile = (owners, named) => {
230
+ use(owners, named, file.fileName);
231
+ };
232
+ if (fileNeeds(project, held, file, programPackages, inFile)) {
227
233
  unanswered.push(file.fileName);
228
234
  }
229
235
  }
@@ -241,14 +247,14 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
241
247
  for (const { file } of configuration.modules) {
242
248
  const names = declaredAt(file.fileName);
243
249
  for (const { node, text } of moduleStrings(file)) {
244
- use(ownersOf(file, held, node), dependenciesNamed(text, names));
250
+ use(ownersOf(file, held, node), dependenciesNamed(text, names), file.fileName);
245
251
  }
246
252
  }
247
253
  for (const { strings, manifestDirs } of configuration.documents) {
248
254
  for (const manifestDir of manifestDirs) {
249
255
  const names = declaredAt(joinPath(manifestDir, "package.json"));
250
256
  for (const text of strings) {
251
- use([PROJECT_OWNER], dependenciesNamed(text, names));
257
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
252
258
  }
253
259
  }
254
260
  }
@@ -256,11 +262,11 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
256
262
  for (const manifestDir of manifestDirs) {
257
263
  const names = declaredAt(joinPath(manifestDir, "package.json"));
258
264
  for (const { text } of moduleStrings(file)) {
259
- use([PROJECT_OWNER], dependenciesNamed(text, names));
265
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
260
266
  }
261
267
  }
262
268
  }
263
- return { packages, uses, unanswered };
269
+ return { uses, at, unanswered };
264
270
  }
265
271
  /** The declarations of the inventory a use written at one node belongs to. */
266
272
  function ownersOf(file, held, node) {
@@ -496,8 +502,10 @@ function installedOf(host, root, name) {
496
502
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
497
503
  if (read !== undefined && isRecord(read.value)) {
498
504
  const bin = read.value["bin"];
505
+ const peers = read.value["peerDependencies"];
499
506
  // A string `bin` names one command, the package's name without its scope.
500
507
  return {
508
+ peers: isRecord(peers) ? Object.keys(peers) : [],
501
509
  commands: typeof bin === "string"
502
510
  ? bin === ""
503
511
  ? []
@@ -558,20 +566,49 @@ function scriptTokensOf(manifest) {
558
566
  function isInside(path, dir) {
559
567
  return dir === "" || path.startsWith(`${dir}/`);
560
568
  }
569
+ /** The directory below the target root a declared dependency's manifest is in, `""` being the root. */
570
+ function manifestDir(dependency) {
571
+ const dir = dirnamePath(dependency.position.path);
572
+ return dir === "." ? "" : dir;
573
+ }
561
574
  /**
562
- * The run's dependency evidence: the target's manifest and each workspace member's in
563
- * `members` below the target root, read with the position of every dependency each
564
- * declares, what the projects and the commands its scripts and workflow steps run need,
565
- * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
566
- * in configurations the run could not build. A manifest the program cannot read declares
567
- * nothing.
575
+ * Adds to `needed` each declared dependency that the installed manifest of a needed or
576
+ * run declared dependency names as a peer, until no further one joins: the declaration of
577
+ * that name in the needing dependency's manifest or in a manifest above it, where the
578
+ * installed peer is resolved from. `peers` holds each declaration's installed peers by
579
+ * {@link declarationKey}.
568
580
  */
569
- export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
581
+ function addNeededPeers(declared, peers, needed, ran) {
582
+ const pending = declared.filter((dependency) => needed.has(declarationKey(dependency)) || ran.has(declarationKey(dependency)));
583
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
584
+ const below = manifestDir(next);
585
+ for (const peer of peers.get(declarationKey(next)) ?? []) {
586
+ for (const dependency of declared) {
587
+ const key = declarationKey(dependency);
588
+ if (dependency.name === peer &&
589
+ !needed.has(key) &&
590
+ (below === manifestDir(dependency) || isInside(below, manifestDir(dependency)))) {
591
+ needed.add(key);
592
+ pending.push(dependency);
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+ /**
599
+ * The run's dependency evidence over the target's manifest and those of `members` and
600
+ * `nested` packages below the root: each declared dependency's position, what the projects,
601
+ * scripts and workflow steps need, and each deletion candidate's last uses. A member in
602
+ * `unbuilt` holds source only in configurations the run could not build. An unreadable
603
+ * manifest declares nothing, nor does a nested package's uninstalled dependency, because
604
+ * no run requires a nested package to be installed.
605
+ */
606
+ export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = [], nested = []) {
570
607
  const root = resolvePath(host.workingDirectory(), targetRoot);
571
608
  const scopeOf = packageScope(host, targetRoot);
572
609
  const manifests = new Map([[root, MANIFEST]]);
573
610
  const unbuilt = new Set();
574
- for (const dir of [...members, ...unbuiltMembers]) {
611
+ for (const dir of [...members, ...unbuiltMembers, ...nested]) {
575
612
  const below = relativePath(root, dir);
576
613
  if (below !== undefined && !manifests.has(dir)) {
577
614
  manifests.set(dir, `${below}/${MANIFEST}`);
@@ -586,6 +623,8 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
586
623
  const workflow = new Set(workflowTokens(host, targetRoot));
587
624
  const declared = [];
588
625
  const ran = new Set();
626
+ const peers = new Map();
627
+ const uninstalledHere = new Set(nested);
589
628
  for (const [dir, path] of manifests) {
590
629
  const read = readJSON(host, joinPath(dir, MANIFEST));
591
630
  if (read === undefined || !isRecord(read.value)) {
@@ -593,12 +632,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
593
632
  }
594
633
  const tokens = scriptTokensOf(read.value);
595
634
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
596
- declared.push(dependency);
597
635
  const found = installedOf(host, dir, dependency.name);
636
+ if (found === undefined && uninstalledHere.has(dir)) {
637
+ continue;
638
+ }
639
+ declared.push(dependency);
598
640
  if (found === undefined) {
599
641
  continue;
600
642
  }
601
- const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
643
+ peers.set(declarationKey(dependency), found.peers);
644
+ const runs = (command) => tokens.has(command) || workflow.has(command);
602
645
  if (found.commands.some(runs)) {
603
646
  ran.add(declarationKey(dependency));
604
647
  }
@@ -607,25 +650,36 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
607
650
  // A declaration several projects compile is one user, and a use any project makes is
608
651
  // a use, so the uses are merged by declaration before anything is counted.
609
652
  const needed = new Set();
653
+ const byName = new Map();
654
+ for (const dependency of declared) {
655
+ byName.set(dependency.name, [...(byName.get(dependency.name) ?? []), dependency]);
656
+ }
610
657
  const uses = new Map();
611
658
  for (const project of perProject) {
612
659
  // A specifier the checker left unanswered may resolve to any dependency a manifest at
613
660
  // or above its file declares, and what the configuration needs is never deleted.
614
661
  const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
615
- const unknown = declared
616
- .filter((dependency) => {
617
- const dir = dirnamePath(dependency.position.path);
618
- return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
619
- })
620
- .map((dependency) => dependency.name);
662
+ const unknownDeclared = declared.filter((dependency) => unanswered.some((file) => isInside(file, manifestDir(dependency))));
663
+ unknownDeclared.forEach((dependency) => needed.add(declarationKey(dependency)));
664
+ const unknown = unknownDeclared.map((dependency) => dependency.name);
621
665
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
622
666
  for (const [id, used] of [...project.uses, ...held]) {
623
667
  const merged = uses.get(id) ?? new Set();
624
668
  used.forEach((one) => merged.add(one));
625
669
  uses.set(id, merged);
626
670
  }
627
- [...project.packages, ...unknown].forEach((one) => needed.add(one));
671
+ for (const [path, named] of project.at) {
672
+ const below = path === ANYWHERE ? undefined : relativePath(root, path);
673
+ for (const one of named) {
674
+ for (const dependency of byName.get(one) ?? []) {
675
+ if (below === undefined || isInside(below, manifestDir(dependency))) {
676
+ needed.add(declarationKey(dependency));
677
+ }
678
+ }
679
+ }
680
+ }
628
681
  }
682
+ addNeededPeers(declared, peers, needed, ran);
629
683
  const users = new Map();
630
684
  for (const used of uses.values()) {
631
685
  used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The members an element access reads by the literal type of its index: `o[k]` with `k`
3
+ * typed `"a" | "b"` reads `a` and `b` of the type of `o`, an index typed `keyof T` reads
4
+ * every member of `T`, and an index typed as a type parameter reads what its constraint does. Where the index is a parameter of a function every call of which the
5
+ * target holds, the members are read at each call, by the type of the argument it passes.
6
+ */
7
+ import { isArrayLiteralExpression, isArrowFunction, isBinaryExpression, isCallExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isFunctionDeclaration, isFunctionExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { nodeKey } from "./inventory.js";
9
+ import { renderPosition } from "./position.js";
10
+ import { UNANSWERED } from "./query.js";
11
+ /** The assignment operators that read their target before they write it. */
12
+ const COMPOUND = new Set([
13
+ SyntaxKind.PlusEqualsToken,
14
+ SyntaxKind.MinusEqualsToken,
15
+ SyntaxKind.AsteriskEqualsToken,
16
+ SyntaxKind.AsteriskAsteriskEqualsToken,
17
+ SyntaxKind.SlashEqualsToken,
18
+ SyntaxKind.PercentEqualsToken,
19
+ SyntaxKind.LessThanLessThanEqualsToken,
20
+ SyntaxKind.GreaterThanGreaterThanEqualsToken,
21
+ SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
22
+ SyntaxKind.AmpersandEqualsToken,
23
+ SyntaxKind.BarEqualsToken,
24
+ SyntaxKind.CaretEqualsToken,
25
+ SyntaxKind.AmpersandAmpersandEqualsToken,
26
+ SyntaxKind.BarBarEqualsToken,
27
+ SyntaxKind.QuestionQuestionEqualsToken,
28
+ ]);
29
+ /** How an expression is used where it is written: read, written, or both. */
30
+ function useOf(node) {
31
+ let at = node;
32
+ while (isParenthesizedExpression(at.parent)) {
33
+ at = at.parent;
34
+ }
35
+ const parent = at.parent;
36
+ if (isBinaryExpression(parent) && parent.left === at) {
37
+ if (parent.operatorToken.kind === SyntaxKind.EqualsToken) {
38
+ return "write";
39
+ }
40
+ if (COMPOUND.has(parent.operatorToken.kind)) {
41
+ return "both";
42
+ }
43
+ }
44
+ if ((isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
45
+ (parent.operator === SyntaxKind.PlusPlusToken || parent.operator === SyntaxKind.MinusMinusToken)) {
46
+ return "both";
47
+ }
48
+ return "read";
49
+ }
50
+ function unwrapped(node) {
51
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
52
+ }
53
+ /** Whether an index is a literal the checker resolves to a member by its own text. */
54
+ function isLiteralIndex(node) {
55
+ const at = unwrapped(node);
56
+ return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
57
+ }
58
+ /**
59
+ * The function `node` is, where every call of it is written in its own file under its own
60
+ * name: a function declaration or a `const` initialized with an arrow function or a function
61
+ * expression, neither exported. A method may be dispatched to through a base class or an
62
+ * interface, so it is none.
63
+ */
64
+ function callableOf(node) {
65
+ const exported = (holder) => (holder.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.ExportKeyword || modifier.kind === SyntaxKind.DefaultKeyword);
66
+ if (isFunctionDeclaration(node)) {
67
+ return node.name === undefined || exported(node)
68
+ ? undefined
69
+ : { name: node.name, parameters: node.parameters, body: node.body };
70
+ }
71
+ if (!isArrowFunction(node) && !isFunctionExpression(node)) {
72
+ return undefined;
73
+ }
74
+ const declaration = node.parent;
75
+ const list = declaration.parent;
76
+ if (!isVariableDeclaration(declaration) ||
77
+ declaration.initializer !== node ||
78
+ !isIdentifier(declaration.name) ||
79
+ !isVariableDeclarationList(list) ||
80
+ (list.flags & NodeFlags.Const) === 0 ||
81
+ !isVariableStatement(list.parent) ||
82
+ exported(list.parent)) {
83
+ return undefined;
84
+ }
85
+ return { name: declaration.name, parameters: node.parameters, body: node.body };
86
+ }
87
+ /** The innermost function enclosing a node, or none. */
88
+ function enclosingFunction(node) {
89
+ for (let at = node.parent; at !== undefined; at = at.parent) {
90
+ if (isFunctionDeclaration(at) ||
91
+ isArrowFunction(at) ||
92
+ isFunctionExpression(at) ||
93
+ at.kind === SyntaxKind.MethodDeclaration ||
94
+ at.kind === SyntaxKind.Constructor ||
95
+ at.kind === SyntaxKind.GetAccessor ||
96
+ at.kind === SyntaxKind.SetAccessor) {
97
+ return at;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /** Whether an assignment target, a destructuring pattern at any depth included, assigns a name. */
103
+ function targetAssigns(target, name) {
104
+ if (isParenthesizedExpression(target)) {
105
+ return targetAssigns(target.expression, name);
106
+ }
107
+ if (isIdentifier(target)) {
108
+ return target.text === name;
109
+ }
110
+ if (isArrayLiteralExpression(target)) {
111
+ return target.elements.some((element) => targetAssigns(element, name));
112
+ }
113
+ if (isObjectLiteralExpression(target)) {
114
+ return target.properties.some((property) => isPropertyAssignment(property)
115
+ ? targetAssigns(property.initializer, name)
116
+ : isShorthandPropertyAssignment(property)
117
+ ? isIdentifier(property.name) && property.name.text === name
118
+ : isSpreadAssignment(property) && targetAssigns(property.expression, name));
119
+ }
120
+ if (isSpreadElement(target)) {
121
+ return targetAssigns(target.expression, name);
122
+ }
123
+ // A pattern element with a default value, `[k = "a"] = …`, assigns its left side.
124
+ return (isBinaryExpression(target) &&
125
+ target.operatorToken.kind === SyntaxKind.EqualsToken &&
126
+ targetAssigns(target.left, name));
127
+ }
128
+ /**
129
+ * Whether a body assigns a name: by an assignment, a destructuring one included, an
130
+ * increment or a decrement, or as the target of a `for…in` or `for…of` loop.
131
+ */
132
+ function assigns(body, name) {
133
+ let found = false;
134
+ const visit = (node) => {
135
+ if (found) {
136
+ return;
137
+ }
138
+ const target = isBinaryExpression(node) &&
139
+ (node.operatorToken.kind === SyntaxKind.EqualsToken || COMPOUND.has(node.operatorToken.kind))
140
+ ? node.left
141
+ : (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) &&
142
+ (node.operator === SyntaxKind.PlusPlusToken ||
143
+ node.operator === SyntaxKind.MinusMinusToken)
144
+ ? node.operand
145
+ : (isForInStatement(node) || isForOfStatement(node)) &&
146
+ node.initializer.kind !== SyntaxKind.VariableDeclarationList
147
+ ? node.initializer
148
+ : undefined;
149
+ found = target !== undefined && targetAssigns(target, name);
150
+ node.forEachChild(visit);
151
+ };
152
+ if (body !== undefined) {
153
+ visit(body);
154
+ }
155
+ return found;
156
+ }
157
+ /**
158
+ * The references the element accesses of one project's own files make to the members their
159
+ * index's literal type names, each made by the declaration holding the access or the call.
160
+ */
161
+ export function elementAccessReferences(project, held, chains, root, testFiles) {
162
+ const queries = project.queries;
163
+ const candidates = [];
164
+ const calls = new Map();
165
+ const names = [];
166
+ for (const file of project.ownSourceFiles()) {
167
+ const fileId = held.declarations.get(nodeKey(file, file));
168
+ if (fileId === undefined) {
169
+ continue;
170
+ }
171
+ const visit = (node, from) => {
172
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
173
+ if (isElementAccessExpression(node) && !isLiteralIndex(node.argumentExpression)) {
174
+ candidates.push({ file, from: holder, access: node, use: useOf(node) });
175
+ }
176
+ if (isCallExpression(node)) {
177
+ calls.set(node, { file, from: holder });
178
+ }
179
+ if (isIdentifier(node)) {
180
+ names.push(node);
181
+ }
182
+ node.forEachChild((child) => {
183
+ visit(child, holder);
184
+ });
185
+ };
186
+ file.forEachChild((child) => {
187
+ visit(child, fileId);
188
+ });
189
+ }
190
+ if (candidates.length === 0) {
191
+ return [];
192
+ }
193
+ // A parameter index is decided at the calls only where the function is a callable every
194
+ // use of which is a call, so the symbols of the parameters and of every name are read.
195
+ const parameterOf = new Map();
196
+ const byCallable = new Map();
197
+ for (const candidate of candidates) {
198
+ const index = unwrapped(candidate.access.argumentExpression);
199
+ const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
200
+ const callable = fn === undefined ? undefined : callableOf(fn);
201
+ if (callable === undefined || !isIdentifier(index)) {
202
+ continue;
203
+ }
204
+ const at = callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === index.text);
205
+ if (at < 0 || assigns(callable.body, index.text)) {
206
+ continue;
207
+ }
208
+ parameterOf.set(candidate, { callable, at });
209
+ byCallable.set(callable.name, [...(byCallable.get(callable.name) ?? []), candidate]);
210
+ }
211
+ const callableNames = [...byCallable.keys()];
212
+ const spelled = new Set(callableNames.map((name) => name.text));
213
+ const sameName = names.filter((name) => spelled.has(name.text));
214
+ const symbols = queries.symbolsAt([...callableNames, ...sameName]);
215
+ const symbolOf = new Map();
216
+ [...callableNames, ...sameName].forEach((node, at) => {
217
+ const symbol = symbols[at];
218
+ if (symbol !== UNANSWERED && symbol !== undefined) {
219
+ symbolOf.set(node, symbol);
220
+ }
221
+ });
222
+ const callsOf = new Map();
223
+ for (const name of callableNames) {
224
+ const symbol = symbolOf.get(name);
225
+ let found = symbol === undefined ? undefined : [];
226
+ for (const use of sameName) {
227
+ if (found === undefined) {
228
+ break;
229
+ }
230
+ if (use === name || symbolOf.get(use) !== symbol) {
231
+ continue;
232
+ }
233
+ const parent = use.parent;
234
+ found =
235
+ isCallExpression(parent) && parent.expression === use ? [...found, parent] : undefined;
236
+ }
237
+ callsOf.set(name, found);
238
+ }
239
+ const lookups = [];
240
+ for (const candidate of candidates) {
241
+ const deferred = parameterOf.get(candidate);
242
+ const sites = deferred === undefined ? undefined : callsOf.get(deferred.callable.name);
243
+ if (deferred === undefined || sites === undefined) {
244
+ lookups.push({
245
+ file: candidate.file,
246
+ from: candidate.from,
247
+ at: candidate.access.argumentExpression,
248
+ object: candidate.access.expression,
249
+ index: candidate.access.argumentExpression,
250
+ use: candidate.use,
251
+ });
252
+ continue;
253
+ }
254
+ // An object that is a parameter too is read as the argument each call passes there.
255
+ const object = unwrapped(candidate.access.expression);
256
+ const objectAt = isIdentifier(object)
257
+ ? deferred.callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text)
258
+ : -1;
259
+ for (const call of sites) {
260
+ const parameter = deferred.callable.parameters[deferred.at];
261
+ const index = call.arguments[deferred.at] ?? parameter?.initializer;
262
+ const site = calls.get(call);
263
+ if (index === undefined || site === undefined) {
264
+ continue;
265
+ }
266
+ lookups.push({
267
+ file: site.file,
268
+ from: site.from,
269
+ at: call,
270
+ object: (objectAt < 0 ? undefined : call.arguments[objectAt]) ?? candidate.access.expression,
271
+ index,
272
+ use: candidate.use,
273
+ });
274
+ }
275
+ }
276
+ const nodes = lookups.flatMap((one) => [one.index, one.object]);
277
+ const types = queries.typesAt(nodes);
278
+ const typeOf = (node, at) => {
279
+ const type = types[at];
280
+ return nodes[at] === node && type !== UNANSWERED ? type : undefined;
281
+ };
282
+ // A type parameter's body serves every instantiation, so it reads what its constraint admits.
283
+ const keysOf = (type) => {
284
+ if (!type.isTypeParameter()) {
285
+ return type;
286
+ }
287
+ const constraint = queries.constraintOf(type);
288
+ return constraint === UNANSWERED || constraint === undefined ? type : constraint;
289
+ };
290
+ const partsOf = (type) => {
291
+ const parts = queries.constituents(type);
292
+ return parts === UNANSWERED ? [type] : parts;
293
+ };
294
+ const found = [];
295
+ lookups.forEach((lookup, at) => {
296
+ const indexType = typeOf(lookup.index, 2 * at);
297
+ const objectType = typeOf(lookup.object, 2 * at + 1);
298
+ const literals = indexType === undefined ? undefined : literalNames(partsOf(keysOf(indexType)));
299
+ if (objectType === undefined || literals === undefined) {
300
+ return;
301
+ }
302
+ const position = renderPosition(lookup.file, root, lookup.at.getStart());
303
+ for (const part of partsOf(objectType)) {
304
+ const properties = queries.propertiesOf(part);
305
+ for (const property of properties === UNANSWERED ? [] : properties) {
306
+ if (!literals.has(property.escapedName)) {
307
+ continue;
308
+ }
309
+ for (const to of chains.declarationsOf(property)) {
310
+ for (const use of lookup.use === "both" ? ["read", "write"] : [lookup.use]) {
311
+ found.push({
312
+ from: lookup.from,
313
+ to,
314
+ position,
315
+ use,
316
+ resolution: "keyed",
317
+ test: testFiles.has(position.path),
318
+ });
319
+ }
320
+ }
321
+ }
322
+ }
323
+ });
324
+ return found;
325
+ }
326
+ /** The names a union of literal types spells, or none where a constituent is no string or number literal. */
327
+ function literalNames(parts) {
328
+ const names = new Set();
329
+ for (const part of parts) {
330
+ if (part.isStringLiteralType()) {
331
+ names.add(part.value);
332
+ }
333
+ else if (part.isNumberLiteralType()) {
334
+ names.add(String(part.value));
335
+ }
336
+ else {
337
+ return undefined;
338
+ }
339
+ }
340
+ return names;
341
+ }