@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.
- package/dist/src/analysis.js +89 -44
- package/dist/src/augmentations.js +14 -12
- package/dist/src/components.js +62 -20
- package/dist/src/configuration-files.js +5 -5
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contextual-uses.js +71 -14
- package/dist/src/convention-rows.js +6 -2
- package/dist/src/conventions.js +3 -1
- package/dist/src/dependencies.js +80 -26
- package/dist/src/element-access.js +341 -0
- package/dist/src/erased-stores.js +317 -0
- package/dist/src/findings/completion.js +46 -15
- package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
- package/dist/src/findings/emitters.js +1 -1
- package/dist/src/findings/self-check.js +17 -4
- package/dist/src/findings-pass.js +6 -5
- package/dist/src/in-operands.js +30 -0
- package/dist/src/indexed-access.js +264 -125
- package/dist/src/intra-function-parts.js +25 -11
- package/dist/src/inventory.js +25 -0
- 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 +12 -1
- package/dist/src/report.js +1 -1
- package/dist/src/reporters/sarif.js +4 -0
- package/dist/src/serialization-contract.js +43 -14
- package/dist/src/session.js +12 -7
- package/dist/src/suppress-file.js +58 -6
- package/dist/src/suppress-inline.js +13 -4
- package/dist/src/suppress.js +54 -24
- 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/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +135 -52
- package/src/augmentations.ts +14 -11
- package/src/components.ts +103 -21
- package/src/configuration-files.ts +5 -4
- package/src/conformance-record.ts +1 -1
- package/src/contextual-uses.ts +81 -17
- package/src/convention-rows.ts +6 -2
- package/src/conventions.ts +10 -1
- package/src/dependencies.ts +107 -30
- package/src/element-access.ts +446 -0
- package/src/erased-stores.ts +413 -0
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +54 -20
- package/src/findings/dependencies-and-module-machinery.ts +1 -1
- package/src/findings/emitters.ts +1 -1
- package/src/findings/self-check.ts +18 -4
- package/src/findings-pass.ts +12 -5
- package/src/in-operands.ts +45 -0
- package/src/indexed-access.ts +303 -145
- package/src/intra-function-parts.ts +41 -16
- package/src/inventory.ts +32 -0
- package/src/keyof-comparisons.ts +211 -0
- package/src/kinds.ts +20 -7
- package/src/manifest.ts +6 -21
- package/src/matrix.ts +34 -2
- package/src/query.ts +10 -0
- package/src/references.ts +23 -1
- package/src/report.ts +3 -1
- package/src/reporters/sarif.ts +4 -0
- package/src/serialization-contract.ts +53 -14
- package/src/session.ts +34 -29
- package/src/suppress-file.ts +68 -5
- package/src/suppress-inline.ts +14 -4
- package/src/suppress.ts +63 -28
- package/src/unimplemented-callees.ts +411 -0
- package/src/value-reach.ts +11 -7
- 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
|
|
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: [
|
|
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
|
];
|
package/dist/src/conventions.js
CHANGED
|
@@ -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) ||
|
|
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);
|
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,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 {
|
|
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
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
[
|
|
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
|
+
}
|