@cplieger/deadset-ts 7.0.0-dev.1 → 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.
- package/dist/src/analysis.js +95 -60
- package/dist/src/augmentations.js +14 -12
- package/dist/src/component-layer.js +1 -1
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +230 -30
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +104 -13
- package/dist/src/convention-rows.js +10 -0
- package/dist/src/conventions.js +35 -1
- package/dist/src/dependencies.js +90 -32
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +78 -16
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -5
- package/dist/src/findings/self-check.js +27 -10
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -119
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +28 -12
- package/dist/src/keyof-comparisons.js +170 -0
- package/dist/src/kinds.js +17 -7
- package/dist/src/manifest.js +6 -16
- package/dist/src/matrix.js +20 -2
- package/dist/src/query.js +3 -0
- package/dist/src/references.js +14 -3
- package/dist/src/report.js +4 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/roots.js +0 -1
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -8
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +14 -5
- package/dist/src/suppress.js +64 -35
- package/dist/src/unimplemented-callees.js +312 -0
- package/dist/src/value-reach.js +7 -7
- package/dist/src/verbs/analyze.js +11 -9
- package/dist/src/workspace-programs.js +0 -3
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +145 -82
- package/src/augmentations.ts +14 -11
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +104 -22
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +250 -31
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +135 -17
- package/src/convention-rows.ts +23 -0
- package/src/conventions.ts +53 -1
- package/src/dependencies.ts +119 -38
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/element-access.ts +446 -0
- package/src/entry-points.ts +1 -1
- package/src/erased-stores.ts +413 -0
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +3 -1
- package/src/finding.ts +3 -1
- package/src/findings/completion.ts +97 -21
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +4 -9
- package/src/findings/self-check.ts +31 -14
- package/src/findings-pass.ts +14 -7
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +1 -1
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -137
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +37 -33
- package/src/json-document.ts +1 -1
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +7 -22
- package/src/matrix.ts +34 -2
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/query.ts +10 -0
- package/src/references.ts +30 -7
- package/src/report.ts +11 -5
- package/src/reporters/sarif.ts +4 -0
- package/src/roots.ts +0 -3
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -31
- package/src/stores.ts +1 -1
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +17 -7
- package/src/suppress.ts +79 -47
- package/src/template-field.ts +1 -1
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +12 -8
- package/src/verbs/analyze.ts +15 -9
- package/src/verbs/verb.ts +1 -4
- package/src/workspace-programs.ts +2 -9
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
package/dist/src/dependencies.js
CHANGED
|
@@ -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
|
|
212
|
+
const at = new Map();
|
|
213
|
+
const use = (owners, named, path) => {
|
|
211
214
|
for (const one of named) {
|
|
212
|
-
|
|
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
|
-
|
|
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,22 +247,26 @@ 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
|
-
for (const
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
for (const { strings, manifestDirs } of configuration.documents) {
|
|
254
|
+
for (const manifestDir of manifestDirs) {
|
|
255
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
256
|
+
for (const text of strings) {
|
|
257
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
|
|
258
|
+
}
|
|
251
259
|
}
|
|
252
260
|
}
|
|
253
|
-
for (const { file,
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
for (const { file, manifestDirs } of configuration.outside) {
|
|
262
|
+
for (const manifestDir of manifestDirs) {
|
|
263
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
264
|
+
for (const { text } of moduleStrings(file)) {
|
|
265
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
|
|
266
|
+
}
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
|
-
return {
|
|
269
|
+
return { uses, at, unanswered };
|
|
260
270
|
}
|
|
261
271
|
/** The declarations of the inventory a use written at one node belongs to. */
|
|
262
272
|
function ownersOf(file, held, node) {
|
|
@@ -492,8 +502,10 @@ function installedOf(host, root, name) {
|
|
|
492
502
|
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
493
503
|
if (read !== undefined && isRecord(read.value)) {
|
|
494
504
|
const bin = read.value["bin"];
|
|
505
|
+
const peers = read.value["peerDependencies"];
|
|
495
506
|
// A string `bin` names one command, the package's name without its scope.
|
|
496
507
|
return {
|
|
508
|
+
peers: isRecord(peers) ? Object.keys(peers) : [],
|
|
497
509
|
commands: typeof bin === "string"
|
|
498
510
|
? bin === ""
|
|
499
511
|
? []
|
|
@@ -554,20 +566,49 @@ function scriptTokensOf(manifest) {
|
|
|
554
566
|
function isInside(path, dir) {
|
|
555
567
|
return dir === "" || path.startsWith(`${dir}/`);
|
|
556
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
|
+
}
|
|
574
|
+
/**
|
|
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}.
|
|
580
|
+
*/
|
|
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
|
+
}
|
|
557
598
|
/**
|
|
558
|
-
* The run's dependency evidence
|
|
559
|
-
* `
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
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.
|
|
564
605
|
*/
|
|
565
|
-
export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
|
|
606
|
+
export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = [], nested = []) {
|
|
566
607
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
567
608
|
const scopeOf = packageScope(host, targetRoot);
|
|
568
609
|
const manifests = new Map([[root, MANIFEST]]);
|
|
569
610
|
const unbuilt = new Set();
|
|
570
|
-
for (const dir of [...members, ...unbuiltMembers]) {
|
|
611
|
+
for (const dir of [...members, ...unbuiltMembers, ...nested]) {
|
|
571
612
|
const below = relativePath(root, dir);
|
|
572
613
|
if (below !== undefined && !manifests.has(dir)) {
|
|
573
614
|
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
@@ -582,6 +623,8 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
582
623
|
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
583
624
|
const declared = [];
|
|
584
625
|
const ran = new Set();
|
|
626
|
+
const peers = new Map();
|
|
627
|
+
const uninstalledHere = new Set(nested);
|
|
585
628
|
for (const [dir, path] of manifests) {
|
|
586
629
|
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
587
630
|
if (read === undefined || !isRecord(read.value)) {
|
|
@@ -589,12 +632,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
589
632
|
}
|
|
590
633
|
const tokens = scriptTokensOf(read.value);
|
|
591
634
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
592
|
-
declared.push(dependency);
|
|
593
635
|
const found = installedOf(host, dir, dependency.name);
|
|
636
|
+
if (found === undefined && uninstalledHere.has(dir)) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
declared.push(dependency);
|
|
594
640
|
if (found === undefined) {
|
|
595
641
|
continue;
|
|
596
642
|
}
|
|
597
|
-
|
|
643
|
+
peers.set(declarationKey(dependency), found.peers);
|
|
644
|
+
const runs = (command) => tokens.has(command) || workflow.has(command);
|
|
598
645
|
if (found.commands.some(runs)) {
|
|
599
646
|
ran.add(declarationKey(dependency));
|
|
600
647
|
}
|
|
@@ -603,25 +650,36 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
|
|
|
603
650
|
// A declaration several projects compile is one user, and a use any project makes is
|
|
604
651
|
// a use, so the uses are merged by declaration before anything is counted.
|
|
605
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
|
+
}
|
|
606
657
|
const uses = new Map();
|
|
607
658
|
for (const project of perProject) {
|
|
608
659
|
// A specifier the checker left unanswered may resolve to any dependency a manifest at
|
|
609
660
|
// or above its file declares, and what the configuration needs is never deleted.
|
|
610
661
|
const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
|
|
615
|
-
})
|
|
616
|
-
.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);
|
|
617
665
|
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
|
|
618
666
|
for (const [id, used] of [...project.uses, ...held]) {
|
|
619
667
|
const merged = uses.get(id) ?? new Set();
|
|
620
668
|
used.forEach((one) => merged.add(one));
|
|
621
669
|
uses.set(id, merged);
|
|
622
670
|
}
|
|
623
|
-
[
|
|
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
|
+
}
|
|
624
681
|
}
|
|
682
|
+
addNeededPeers(declared, peers, needed, ran);
|
|
625
683
|
const users = new Map();
|
|
626
684
|
for (const used of uses.values()) {
|
|
627
685
|
used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));
|
package/dist/src/discover.js
CHANGED
|
@@ -23,7 +23,7 @@ export class DiscoveryError extends Error {
|
|
|
23
23
|
/** The category an error diagnostic carries, which the protocol sends as a number. */
|
|
24
24
|
const ERROR_CATEGORY = DiagnosticCategory.Error;
|
|
25
25
|
/** Whether one diagnostic is an error, which is what fails a run closed. */
|
|
26
|
-
|
|
26
|
+
function isError(diagnostic) {
|
|
27
27
|
return diagnostic.category === ERROR_CATEGORY;
|
|
28
28
|
}
|
|
29
29
|
/**
|
package/dist/src/edges.js
CHANGED
|
@@ -25,7 +25,7 @@ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and a
|
|
|
25
25
|
const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
|
|
26
26
|
const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
|
|
27
27
|
/** An edges document the Contract refuses: a malformed input, answered with the usage code. */
|
|
28
|
-
|
|
28
|
+
class EdgesError extends ConfigError {
|
|
29
29
|
constructor(at, text, want) {
|
|
30
30
|
super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
|
|
31
31
|
this.name = "EdgesError";
|
|
@@ -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
|
+
}
|