fallow-type-aware 3.35.0 → 3.36.0
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/package.json +10 -1
- package/src/backend-preflight.mjs +33 -4
- package/src/backend-selection.mjs +18 -0
- package/src/backend.mjs +169 -0
- package/src/computed-member-access.mjs +45 -0
- package/src/generated-protocol.mjs +12 -0
- package/src/mutant-typecheck.mjs +13 -10
- package/src/namespace-uses.mjs +499 -0
- package/src/project-package.mjs +37 -0
- package/src/project-state.mjs +12 -10
- package/src/protocol.mjs +7 -16
- package/src/reference-names.mjs +18 -9
- package/src/semantic-identity.mjs +7 -5
- package/src/semantic.mjs +71 -133
- package/src/shorthand-references.mjs +243 -0
- package/src/symbol-impact.mjs +3 -6
- package/src/symbol-targets.mjs +7 -1
package/src/reference-names.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ast } from "./backend.mjs";
|
|
2
|
+
|
|
3
|
+
const {
|
|
2
4
|
isExportDeclaration,
|
|
3
5
|
isImportDeclaration,
|
|
4
6
|
isImportEqualsDeclaration,
|
|
@@ -7,7 +9,7 @@ import {
|
|
|
7
9
|
isNamedExports,
|
|
8
10
|
isNamespaceExportDeclaration,
|
|
9
11
|
isNamespaceImport,
|
|
10
|
-
}
|
|
12
|
+
} = ast;
|
|
11
13
|
|
|
12
14
|
const JAVASCRIPT_FILE = /\.[cm]?jsx?$/iu;
|
|
13
15
|
|
|
@@ -28,9 +30,10 @@ const exportNames = ({ exportClause }) => {
|
|
|
28
30
|
return [exportClause.name];
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/** Returns the statements of a `namespace` or `declare module` body, also when nested. */
|
|
34
|
+
export const moduleBodyStatements = (body) => {
|
|
35
|
+
if (body && isModuleDeclaration(body)) return moduleBodyStatements(body.body);
|
|
36
|
+
return body && isModuleBlock(body) ? body.statements : [];
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
const statementAliasNames = (statement) => {
|
|
@@ -39,26 +42,32 @@ const statementAliasNames = (statement) => {
|
|
|
39
42
|
return [statement.name];
|
|
40
43
|
}
|
|
41
44
|
if (isExportDeclaration(statement)) return exportNames(statement);
|
|
42
|
-
if (isModuleDeclaration(statement))
|
|
45
|
+
if (isModuleDeclaration(statement)) {
|
|
46
|
+
return moduleBodyStatements(statement.body).flatMap(statementAliasNames);
|
|
47
|
+
}
|
|
43
48
|
return [];
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
// Default-library and external-library files sit below node_modules. The
|
|
47
52
|
// path check skips them before the source file and its metadata, which each
|
|
48
53
|
// cost a checker round trip.
|
|
49
|
-
const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/u;
|
|
54
|
+
export const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/u;
|
|
50
55
|
|
|
51
56
|
const isOwnSourceFile = (project, sourceFile) =>
|
|
52
57
|
Boolean(sourceFile) &&
|
|
53
58
|
!project.program.isSourceFileDefaultLibrary(sourceFile) &&
|
|
54
59
|
!project.program.isSourceFileFromExternalLibrary(sourceFile);
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
/** Returns the program files outside node_modules and the default libraries. */
|
|
62
|
+
export const ownSourceFiles = (project) =>
|
|
57
63
|
project.program
|
|
58
64
|
.getSourceFileNames()
|
|
59
65
|
.filter((fileName) => !NODE_MODULES_SEGMENT.test(fileName))
|
|
60
66
|
.map((fileName) => project.program.getSourceFile(fileName))
|
|
61
|
-
.filter((sourceFile) => isOwnSourceFile(project, sourceFile))
|
|
67
|
+
.filter((sourceFile) => isOwnSourceFile(project, sourceFile));
|
|
68
|
+
|
|
69
|
+
const programAliasNames = (project) =>
|
|
70
|
+
ownSourceFiles(project)
|
|
62
71
|
.flatMap((sourceFile) => sourceFile.statements.flatMap(statementAliasNames))
|
|
63
72
|
.map((name) => name?.text)
|
|
64
73
|
.filter((text) => typeof text === "string");
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { sync, ast } from "./backend.mjs";
|
|
4
|
+
|
|
5
|
+
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
6
|
+
|
|
7
|
+
const { SymbolFlags } = sync;
|
|
8
|
+
const {
|
|
5
9
|
isClassDeclaration,
|
|
6
10
|
isClassExpression,
|
|
7
11
|
isEnumDeclaration,
|
|
@@ -19,9 +23,7 @@ import {
|
|
|
19
23
|
isSourceFile,
|
|
20
24
|
isTypeAliasDeclaration,
|
|
21
25
|
isVariableDeclaration,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
26
|
+
} = ast;
|
|
25
27
|
|
|
26
28
|
const DECLARATION_KIND_RULES = [
|
|
27
29
|
[[isClassDeclaration, isClassExpression], "class"],
|
package/src/semantic.mjs
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
isCallExpression,
|
|
8
|
-
isClassDeclaration,
|
|
9
|
-
isClassExpression,
|
|
10
|
-
isDecorator,
|
|
11
|
-
isElementAccessExpression,
|
|
12
|
-
isExportDeclaration,
|
|
13
|
-
isExportSpecifier,
|
|
14
|
-
isFunctionBody,
|
|
15
|
-
isGetAccessorDeclaration,
|
|
16
|
-
isIdentifier,
|
|
17
|
-
isImportDeclaration,
|
|
18
|
-
isImportSpecifier,
|
|
19
|
-
isImportTypeNode,
|
|
20
|
-
isNamespaceImport,
|
|
21
|
-
isPropertyAccessExpression,
|
|
22
|
-
isQualifiedName,
|
|
23
|
-
isPrivateIdentifier,
|
|
24
|
-
isSetAccessorDeclaration,
|
|
25
|
-
isStringLiteralLikeNode,
|
|
26
|
-
isTypeNode,
|
|
27
|
-
isTypeQueryNode,
|
|
28
|
-
} from "typescript/unstable/ast/is";
|
|
5
|
+
import { ast, createApi as createBackendApi, heritageTypeName, sync } from "./backend.mjs";
|
|
29
6
|
|
|
30
7
|
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
8
|
+
import { packagePathTargets, projectPackage } from "./project-package.mjs";
|
|
31
9
|
import { projectResult, projectState } from "./project-state.mjs";
|
|
32
10
|
import {
|
|
33
11
|
declarationName,
|
|
@@ -51,9 +29,49 @@ import {
|
|
|
51
29
|
} from "./semantic-identity.mjs";
|
|
52
30
|
import { analyzeSymbolImpact } from "./symbol-impact.mjs";
|
|
53
31
|
import { isJavaScriptFile, referenceNames } from "./reference-names.mjs";
|
|
54
|
-
import {
|
|
32
|
+
import { recordComputedMemberAccess } from "./computed-member-access.mjs";
|
|
33
|
+
import {
|
|
34
|
+
namespaceSourceIdentity,
|
|
35
|
+
recordDynamicImportUncertainty,
|
|
36
|
+
recordNamespaceUse,
|
|
37
|
+
} from "./namespace-uses.mjs";
|
|
38
|
+
import {
|
|
39
|
+
isLiteralMemberName,
|
|
40
|
+
qualifierExpression,
|
|
41
|
+
referenceValueSymbol,
|
|
42
|
+
shorthandReferenceSymbols,
|
|
43
|
+
} from "./shorthand-references.mjs";
|
|
44
|
+
import {
|
|
45
|
+
matchingAliasEntries,
|
|
46
|
+
matchingSymbolEntries,
|
|
47
|
+
recordSymbolUncertainty,
|
|
48
|
+
targetName,
|
|
49
|
+
} from "./symbol-targets.mjs";
|
|
55
50
|
import { analyzeTypeCoupling } from "./type-coupling.mjs";
|
|
56
51
|
|
|
52
|
+
const { SignatureKind, SymbolFlags } = sync;
|
|
53
|
+
const {
|
|
54
|
+
isCallExpression,
|
|
55
|
+
isClassDeclaration,
|
|
56
|
+
isClassExpression,
|
|
57
|
+
isDecorator,
|
|
58
|
+
isExportDeclaration,
|
|
59
|
+
isExportSpecifier,
|
|
60
|
+
isFunctionBody,
|
|
61
|
+
isGetAccessorDeclaration,
|
|
62
|
+
isIdentifier,
|
|
63
|
+
isImportDeclaration,
|
|
64
|
+
isImportSpecifier,
|
|
65
|
+
isImportTypeNode,
|
|
66
|
+
isNamespaceImport,
|
|
67
|
+
isPropertyAccessExpression,
|
|
68
|
+
isPrivateIdentifier,
|
|
69
|
+
isSetAccessorDeclaration,
|
|
70
|
+
isStringLiteralLikeNode,
|
|
71
|
+
isTypeNode,
|
|
72
|
+
isTypeQueryNode,
|
|
73
|
+
} = ast;
|
|
74
|
+
|
|
57
75
|
const ATTACHED_COMMENT_PATTERN = /\/\/|\/\*/u;
|
|
58
76
|
const MAX_SYMBOL_USE_EVIDENCE = 1;
|
|
59
77
|
const DECLARATION_KINDS = new Set([
|
|
@@ -358,13 +376,10 @@ const candidateNameMatches = (candidateNames, name) => !candidateNames || candid
|
|
|
358
376
|
const isIdentifierReference = (node, names) =>
|
|
359
377
|
isIdentifier(node) && candidateNameMatches(names, node.text);
|
|
360
378
|
|
|
361
|
-
const isElementNameNode = (node) =>
|
|
362
|
-
isStringLiteralLikeNode(node) &&
|
|
363
|
-
isElementAccessExpression(node.parent) &&
|
|
364
|
-
node.parent.argumentExpression === node;
|
|
365
|
-
|
|
366
379
|
const isElementReference = (node, candidateNames) =>
|
|
367
|
-
|
|
380
|
+
isStringLiteralLikeNode(node) &&
|
|
381
|
+
candidateNameMatches(candidateNames, node.text) &&
|
|
382
|
+
isLiteralMemberName(node);
|
|
368
383
|
|
|
369
384
|
const entriesByName = (entries) => {
|
|
370
385
|
const byName = new Map();
|
|
@@ -478,9 +493,6 @@ const importTypeSpecifier = (node) => {
|
|
|
478
493
|
return literal && isStringLiteralLikeNode(literal) ? literal : undefined;
|
|
479
494
|
};
|
|
480
495
|
|
|
481
|
-
const isDynamicImportCall = (node) =>
|
|
482
|
-
isCallExpression(node) && node.expression.getText(node.getSourceFile()) === "import";
|
|
483
|
-
|
|
484
496
|
const MODULE_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
485
497
|
|
|
486
498
|
const lexicalModuleIdentityForText = (project, sourceFile, specifierText) => {
|
|
@@ -516,19 +528,8 @@ const moduleIdentityForSpecifier = (project, specifier) => {
|
|
|
516
528
|
};
|
|
517
529
|
|
|
518
530
|
const namespaceImportDeclaration = (project, node) => {
|
|
519
|
-
const
|
|
520
|
-
if (!
|
|
521
|
-
const namespace = isPropertyAccessExpression(parent)
|
|
522
|
-
? parent.name === node
|
|
523
|
-
? parent.expression
|
|
524
|
-
: undefined
|
|
525
|
-
: isElementAccessExpression(parent) && parent.argumentExpression === node
|
|
526
|
-
? parent.expression
|
|
527
|
-
: isQualifiedName(parent) && parent.right === node
|
|
528
|
-
? parent.left
|
|
529
|
-
: undefined;
|
|
530
|
-
if (!namespace) return undefined;
|
|
531
|
-
if (!isIdentifier(namespace)) return undefined;
|
|
531
|
+
const namespace = qualifierExpression(node);
|
|
532
|
+
if (!namespace || !isIdentifier(namespace)) return undefined;
|
|
532
533
|
const symbol = project.checker.getSymbolAtLocation(namespace);
|
|
533
534
|
return symbol?.declarations
|
|
534
535
|
?.map((declaration) => declaration.resolve(project))
|
|
@@ -536,7 +537,7 @@ const namespaceImportDeclaration = (project, node) => {
|
|
|
536
537
|
};
|
|
537
538
|
|
|
538
539
|
const localAliasModuleEdgeDeclaration = (project, node) => {
|
|
539
|
-
const symbol = project
|
|
540
|
+
const symbol = referenceValueSymbol(project, node);
|
|
540
541
|
return symbol?.declarations
|
|
541
542
|
?.map((declaration) => moduleEdgeDeclaration(declaration.resolve(project)))
|
|
542
543
|
.find(Boolean);
|
|
@@ -567,7 +568,8 @@ const referencedModuleIdentity = (project, node) => {
|
|
|
567
568
|
|
|
568
569
|
const isExactExportReference = (project, entry, node) => {
|
|
569
570
|
if (entry.query.symbol.declarationKind !== "export") return true;
|
|
570
|
-
const referencedModule =
|
|
571
|
+
const referencedModule =
|
|
572
|
+
referencedModuleIdentity(project, node) ?? namespaceSourceIdentity(project, node);
|
|
571
573
|
if (!referencedModule) return false;
|
|
572
574
|
const queryModule = canonicalFileIdentity(entry.query.symbol.absolutePath);
|
|
573
575
|
return (
|
|
@@ -612,58 +614,17 @@ const classMemberEntrySetForProject = (state, project) => {
|
|
|
612
614
|
return entries;
|
|
613
615
|
};
|
|
614
616
|
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
const isExactElementReference = (node) => isElementNameNode(node) && typeof node.text === "string";
|
|
617
|
+
const isExactElementReference = (node) =>
|
|
618
|
+
isLiteralMemberName(node) && typeof node.text === "string";
|
|
620
619
|
|
|
621
|
-
const
|
|
622
|
-
if (!isStringLiteralLikeNode(node) || isExactElementReference(node)) return;
|
|
620
|
+
const recordNameUncertainty = (state, projectEntries, node) =>
|
|
623
621
|
(state.classMemberEntriesByName.get(node.text) ?? [])
|
|
624
622
|
.filter((entry) => projectEntries.has(entry))
|
|
625
623
|
.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
const recordComputedMemberAccess = (state, project, entries, node) => {
|
|
629
|
-
if (
|
|
630
|
-
entries.size === 0 ||
|
|
631
|
-
state.projectsWithUnknownDynamicAccess.has(project) ||
|
|
632
|
-
!isElementAccessExpression(node) ||
|
|
633
|
-
node.argumentExpression === undefined ||
|
|
634
|
-
isStringLiteralLikeNode(node.argumentExpression)
|
|
635
|
-
) {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const receiverType = project.checker.getTypeAtLocation(node.expression);
|
|
639
|
-
const receiverProperties = project.checker.getPropertiesOfType(receiverType);
|
|
640
|
-
if (receiverProperties.length === 0) {
|
|
641
|
-
entries.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
|
|
642
|
-
state.projectsWithUnknownDynamicAccess.add(project);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
const matched = new Set(
|
|
646
|
-
receiverProperties
|
|
647
|
-
.filter((property) => state.candidateNames.has(property.name))
|
|
648
|
-
.flatMap((property) => [...matchingSymbolEntries(project, property, "value", state)]),
|
|
649
|
-
);
|
|
650
|
-
matched.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
|
|
651
|
-
};
|
|
652
624
|
|
|
653
|
-
const
|
|
654
|
-
if (!
|
|
655
|
-
|
|
656
|
-
if (!specifier || !isStringLiteralLikeNode(specifier)) {
|
|
657
|
-
(state.exportEntriesByProject.get(project) ?? []).forEach((entry) =>
|
|
658
|
-
recordSymbolUncertainty(state, entry, "dynamic-behavior"),
|
|
659
|
-
);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
const moduleIdentity = moduleIdentityForSpecifier(project, specifier);
|
|
663
|
-
if (!moduleIdentity) return;
|
|
664
|
-
(state.exportEntriesByModule.get(moduleIdentity) ?? []).forEach((entry) =>
|
|
665
|
-
recordSymbolUncertainty(state, entry, "dynamic-behavior"),
|
|
666
|
-
);
|
|
625
|
+
const recordStringDispatchedMemberAccess = (state, projectEntries, node) => {
|
|
626
|
+
if (!isStringLiteralLikeNode(node) || isExactElementReference(node)) return;
|
|
627
|
+
recordNameUncertainty(state, projectEntries, node);
|
|
667
628
|
};
|
|
668
629
|
|
|
669
630
|
const referenceNamespaces = (project, node, rawSymbol) => {
|
|
@@ -706,7 +667,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, names) => {
|
|
|
706
667
|
const projectEntries = classMemberEntrySetForProject(state, project);
|
|
707
668
|
const nodes = [];
|
|
708
669
|
visit(sourceFile, (node) => {
|
|
709
|
-
recordDynamicImportUncertainty(state, project, node);
|
|
670
|
+
recordDynamicImportUncertainty(state, project, node, moduleIdentityForSpecifier);
|
|
710
671
|
if (projectEntries.size > 0) {
|
|
711
672
|
recordStringDispatchedMemberAccess(state, projectEntries, node);
|
|
712
673
|
recordComputedMemberAccess(state, project, projectEntries, node);
|
|
@@ -718,11 +679,16 @@ const scanSymbolUseFile = (root, state, project, sourceFile, names) => {
|
|
|
718
679
|
if (isElementReference(node, state.candidateNames)) nodes.push(node);
|
|
719
680
|
});
|
|
720
681
|
const symbols = project.checker.getSymbolAtLocation(nodes);
|
|
682
|
+
const references = shorthandReferenceSymbols(project, nodes, symbols, state);
|
|
683
|
+
references.unresolved.forEach((node) => recordNameUncertainty(state, projectEntries, node));
|
|
721
684
|
nodes.forEach((node, index) => {
|
|
722
685
|
const rawSymbol = symbols[index];
|
|
686
|
+
recordNamespaceUse(state, project, node, rawSymbol);
|
|
723
687
|
referenceNamespaces(project, node, rawSymbol).forEach((namespace) => {
|
|
724
688
|
const entries = new Set([
|
|
725
|
-
...
|
|
689
|
+
...references.symbols[index].flatMap((symbol) => [
|
|
690
|
+
...matchingAliasEntries(project, symbol, namespace, state),
|
|
691
|
+
]),
|
|
726
692
|
...surfaceEntriesForNode(state, project, node, namespace),
|
|
727
693
|
]);
|
|
728
694
|
entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
|
|
@@ -972,7 +938,7 @@ const frameworkContractRelationsFor = (root, project, declaration, contracts, me
|
|
|
972
938
|
: "extends";
|
|
973
939
|
for (const heritageType of clause.types ?? []) {
|
|
974
940
|
const rawSymbol =
|
|
975
|
-
project.checker.getSymbolAtLocation(heritageType
|
|
941
|
+
project.checker.getSymbolAtLocation(heritageTypeName(heritageType)) ??
|
|
976
942
|
project.checker.getTypeAtLocation(heritageType).aliasSymbol ??
|
|
977
943
|
project.checker.getTypeAtLocation(heritageType).symbol;
|
|
978
944
|
const symbol = resolveAlias(project.checker, rawSymbol);
|
|
@@ -1253,31 +1219,6 @@ const wildcardPattern = (target) => {
|
|
|
1253
1219
|
return new RegExp(`^${escaped}$`, "u");
|
|
1254
1220
|
};
|
|
1255
1221
|
|
|
1256
|
-
const pathWithin = (root, candidate) =>
|
|
1257
|
-
candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
1258
|
-
|
|
1259
|
-
const packageAt = (directory) => {
|
|
1260
|
-
const packageFile = path.join(directory, "package.json");
|
|
1261
|
-
if (!existsSync(packageFile)) return undefined;
|
|
1262
|
-
try {
|
|
1263
|
-
return { root: directory, json: JSON.parse(readFileSync(packageFile, "utf8")) };
|
|
1264
|
-
} catch {
|
|
1265
|
-
return { root: directory, json: {} };
|
|
1266
|
-
}
|
|
1267
|
-
};
|
|
1268
|
-
|
|
1269
|
-
const projectPackage = (root, project) => {
|
|
1270
|
-
const normalizedRoot = path.resolve(root);
|
|
1271
|
-
let current = path.dirname(project.configFileName);
|
|
1272
|
-
while (pathWithin(normalizedRoot, current)) {
|
|
1273
|
-
const found = packageAt(current);
|
|
1274
|
-
if (found) return found;
|
|
1275
|
-
if (current === normalizedRoot) break;
|
|
1276
|
-
current = path.dirname(current);
|
|
1277
|
-
}
|
|
1278
|
-
return { root: normalizedRoot, json: {} };
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1281
1222
|
const ancestorRoots = (root, start) => {
|
|
1282
1223
|
const normalizedRoot = path.resolve(root);
|
|
1283
1224
|
const roots = [];
|
|
@@ -1345,12 +1286,6 @@ const addPathEntry = (entries, sourceFiles, roots, target) => {
|
|
|
1345
1286
|
);
|
|
1346
1287
|
};
|
|
1347
1288
|
|
|
1348
|
-
const packagePathTargets = (project, packageJson) => {
|
|
1349
|
-
const packageName = packageJson.name;
|
|
1350
|
-
if (typeof packageName !== "string") return [];
|
|
1351
|
-
return project.compilerOptions.paths?.[packageName] ?? [];
|
|
1352
|
-
};
|
|
1353
|
-
|
|
1354
1289
|
const discoverEntryPoints = (root, project, requested) => {
|
|
1355
1290
|
const sourceFiles = sourceFileIndex(project);
|
|
1356
1291
|
if (requested.length > 0) return requestedEntryPoints(requested, sourceFiles);
|
|
@@ -2164,7 +2099,7 @@ const executeDisposableSemanticAnalysis = (input) => {
|
|
|
2164
2099
|
|
|
2165
2100
|
export const analyzeSemanticQueries = (
|
|
2166
2101
|
{ root, projects, queries, evidenceLimit },
|
|
2167
|
-
{ createApi = (cwd) =>
|
|
2102
|
+
{ createApi = (cwd) => createBackendApi({ cwd }) } = {},
|
|
2168
2103
|
) => {
|
|
2169
2104
|
if (queries.length === 0) return emptyRequestedAnalysis();
|
|
2170
2105
|
const setupStartedAt = performance.now();
|
|
@@ -2183,7 +2118,10 @@ export const analyzeSemanticQueries = (
|
|
|
2183
2118
|
}
|
|
2184
2119
|
};
|
|
2185
2120
|
|
|
2186
|
-
export const createSemanticSession = (
|
|
2121
|
+
export const createSemanticSession = (
|
|
2122
|
+
root,
|
|
2123
|
+
{ createApi = (cwd) => createBackendApi({ cwd }) } = {},
|
|
2124
|
+
) => {
|
|
2187
2125
|
const api = createApi(root);
|
|
2188
2126
|
const state = {
|
|
2189
2127
|
openFiles: new Set(),
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { SyntaxKind, ast } from "./backend.mjs";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isArrayLiteralExpression,
|
|
5
|
+
isBinaryExpression,
|
|
6
|
+
isBindingElement,
|
|
7
|
+
isComputedPropertyName,
|
|
8
|
+
isElementAccessExpression,
|
|
9
|
+
isForInStatement,
|
|
10
|
+
isForOfStatement,
|
|
11
|
+
isIdentifier,
|
|
12
|
+
isImportClause,
|
|
13
|
+
isImportEqualsDeclaration,
|
|
14
|
+
isImportSpecifier,
|
|
15
|
+
isObjectBindingPattern,
|
|
16
|
+
isObjectLiteralExpression,
|
|
17
|
+
isParenthesizedExpression,
|
|
18
|
+
isPropertyAccessExpression,
|
|
19
|
+
isPropertyAssignment,
|
|
20
|
+
isQualifiedName,
|
|
21
|
+
isShorthandPropertyAssignment,
|
|
22
|
+
isSpreadAssignment,
|
|
23
|
+
isSpreadElement,
|
|
24
|
+
isStringLiteralLikeNode,
|
|
25
|
+
isVariableDeclaration,
|
|
26
|
+
} = ast;
|
|
27
|
+
|
|
28
|
+
// A shorthand name is one identifier with two meanings. The checker resolves
|
|
29
|
+
// it to the local binding, so the property it reads, or the import it
|
|
30
|
+
// forwards, needs a separate lookup.
|
|
31
|
+
|
|
32
|
+
// A literal key can sit in a computed name, such as `{ ["name"]: local }`.
|
|
33
|
+
const keyNode = (node) => (isComputedPropertyName(node.parent) ? node.parent : node);
|
|
34
|
+
|
|
35
|
+
const bindingKeyPattern = (node) => {
|
|
36
|
+
const key = keyNode(node);
|
|
37
|
+
const element = key.parent;
|
|
38
|
+
if (!isBindingElement(element) || !isObjectBindingPattern(element.parent)) return undefined;
|
|
39
|
+
const isKey = element.propertyName
|
|
40
|
+
? element.propertyName === key
|
|
41
|
+
: element.name === key && !element.dotDotDotToken;
|
|
42
|
+
return isKey ? element.parent : undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// The checker already resolves an identifier key that renames, such as `name`
|
|
46
|
+
// in `{ name: local }`, to the property.
|
|
47
|
+
const isRenamingKey = (node) =>
|
|
48
|
+
isIdentifier(node) && isBindingElement(node.parent) && node.parent.propertyName === node;
|
|
49
|
+
|
|
50
|
+
const assignmentKeyProperty = (node) => {
|
|
51
|
+
const key = keyNode(node);
|
|
52
|
+
const property = key.parent;
|
|
53
|
+
return (isShorthandPropertyAssignment(property) || isPropertyAssignment(property)) &&
|
|
54
|
+
property.name === key
|
|
55
|
+
? property
|
|
56
|
+
: undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isShorthandPropertyName = (node) =>
|
|
60
|
+
isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
|
|
61
|
+
|
|
62
|
+
/** Returns the symbol of the value that a name reads, also for a shorthand property. */
|
|
63
|
+
export const referenceValueSymbol = (project, node) =>
|
|
64
|
+
isShorthandPropertyName(node)
|
|
65
|
+
? project.checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
66
|
+
: project.checker.getSymbolAtLocation(node);
|
|
67
|
+
|
|
68
|
+
// Both `const { name } = ns` and `const { name: local } = ns` read `name`
|
|
69
|
+
// from `ns`.
|
|
70
|
+
const destructuredExpression = (node) => {
|
|
71
|
+
const element = node.parent;
|
|
72
|
+
const readName = element.propertyName ?? element.name;
|
|
73
|
+
const declaration = element.parent?.parent;
|
|
74
|
+
return readName === node && isVariableDeclaration(declaration)
|
|
75
|
+
? declaration.initializer
|
|
76
|
+
: undefined;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const QUALIFIER_RULES = [
|
|
80
|
+
[isBindingElement, (parent, node) => destructuredExpression(node)],
|
|
81
|
+
[
|
|
82
|
+
isPropertyAccessExpression,
|
|
83
|
+
(parent, node) => (parent.name === node ? parent.expression : undefined),
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
isElementAccessExpression,
|
|
87
|
+
(parent, node) => (parent.argumentExpression === node ? parent.expression : undefined),
|
|
88
|
+
],
|
|
89
|
+
[isQualifiedName, (parent, node) => (parent.right === node ? parent.left : undefined)],
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/** Returns the expression that qualifies a name, such as `ns` in `ns.name`. */
|
|
93
|
+
export const qualifierExpression = (node) => {
|
|
94
|
+
const parent = node.parent;
|
|
95
|
+
const rule = parent && QUALIFIER_RULES.find(([matches]) => matches(parent));
|
|
96
|
+
return rule?.[1](parent, node);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isImportBinding = (node) =>
|
|
100
|
+
isImportSpecifier(node) || isImportClause(node) || isImportEqualsDeclaration(node);
|
|
101
|
+
|
|
102
|
+
const importBindingNames = (nodes) =>
|
|
103
|
+
new Set(
|
|
104
|
+
nodes
|
|
105
|
+
.filter((node) => isImportBinding(node.parent) && node.parent.name === node)
|
|
106
|
+
.map((node) => node.text),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const isEqualsAssignmentOf = (parent, node) =>
|
|
110
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === node;
|
|
111
|
+
const isInitializerOf = (parent, node) => parent.initializer === node;
|
|
112
|
+
|
|
113
|
+
const ASSIGNMENT_TARGET_RULES = [
|
|
114
|
+
[isBinaryExpression, isEqualsAssignmentOf],
|
|
115
|
+
[isForOfStatement, isInitializerOf],
|
|
116
|
+
[isForInStatement, isInitializerOf],
|
|
117
|
+
[
|
|
118
|
+
isPropertyAssignment,
|
|
119
|
+
(parent, node) => isInitializerOf(parent, node) && isAssignmentTarget(parent.parent),
|
|
120
|
+
],
|
|
121
|
+
[isArrayLiteralExpression, (parent) => isAssignmentTarget(parent)],
|
|
122
|
+
[isParenthesizedExpression, (parent) => isAssignmentTarget(parent)],
|
|
123
|
+
[isSpreadElement, (parent) => isAssignmentTarget(parent)],
|
|
124
|
+
[isSpreadAssignment, (parent) => isAssignmentTarget(parent)],
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/** Returns true when a node is the target of an assignment, such as a pattern in `({ a } = b)`. */
|
|
128
|
+
export const isAssignmentTarget = (node) => {
|
|
129
|
+
const parent = node.parent;
|
|
130
|
+
const rule = parent && ASSIGNMENT_TARGET_RULES.find(([matches]) => matches(parent));
|
|
131
|
+
return rule ? rule[1](parent, node) : false;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const propertyNameText = (name) =>
|
|
135
|
+
isIdentifier(name) || isStringLiteralLikeNode(name) ? name.text : undefined;
|
|
136
|
+
|
|
137
|
+
// Only a direct `=` source and nested object patterns resolve. Array and loop
|
|
138
|
+
// patterns give undefined, and the caller records an uncertainty for them.
|
|
139
|
+
const assignmentPatternType = (project, literal) => {
|
|
140
|
+
const parent = literal.parent;
|
|
141
|
+
if (isBinaryExpression(parent)) {
|
|
142
|
+
// A binary that is itself a pattern target is a default value, and the
|
|
143
|
+
// pattern also reads the outer source.
|
|
144
|
+
return isAssignmentTarget(parent) ? undefined : project.checker.getTypeAtLocation(parent.right);
|
|
145
|
+
}
|
|
146
|
+
if (!isPropertyAssignment(parent) || !isObjectLiteralExpression(parent.parent)) return undefined;
|
|
147
|
+
const outer = assignmentPatternType(project, parent.parent);
|
|
148
|
+
const name = propertyNameText(parent.name);
|
|
149
|
+
if (!outer || name === undefined) return undefined;
|
|
150
|
+
const property = project.checker.getPropertyOfType(outer, name);
|
|
151
|
+
return property ? project.checker.getTypeOfSymbolAtLocation(property, parent) : undefined;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const bindingPropertySymbols = (project, nodes, symbolsByNode) => {
|
|
155
|
+
if (nodes.length === 0) return;
|
|
156
|
+
const types = project.checker.getTypeAtLocation(nodes.map(bindingKeyPattern));
|
|
157
|
+
nodes.forEach((node, index) => {
|
|
158
|
+
const property = types[index] && project.checker.getPropertyOfType(types[index], node.text);
|
|
159
|
+
if (property) symbolsByNode.set(node, [property]);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const assignmentPropertySymbol = (project, node) => {
|
|
164
|
+
const type = assignmentPatternType(project, assignmentKeyProperty(node).parent);
|
|
165
|
+
return type ? project.checker.getPropertyOfType(type, node.text) : undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const isPropertyName = (node) => assignmentKeyProperty(node) !== undefined;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the object binding pattern or the assignment pattern whose source a
|
|
172
|
+
* key reads, such as `{ name }` in `const { name } = source`.
|
|
173
|
+
*/
|
|
174
|
+
export const patternOfKey = (node) => {
|
|
175
|
+
const pattern = bindingKeyPattern(node);
|
|
176
|
+
if (pattern) return pattern;
|
|
177
|
+
const literal = assignmentKeyProperty(node)?.parent;
|
|
178
|
+
return literal && isAssignmentTarget(literal) ? literal : undefined;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Returns the type of the source that a binding or an assignment pattern reads. */
|
|
182
|
+
export const patternSourceType = (project, pattern) =>
|
|
183
|
+
isObjectBindingPattern(pattern)
|
|
184
|
+
? project.checker.getTypeAtLocation(pattern)
|
|
185
|
+
: assignmentPatternType(project, pattern);
|
|
186
|
+
|
|
187
|
+
/** Returns true when a string literal names the member that an access or a pattern reads. */
|
|
188
|
+
export const isLiteralMemberName = (node) =>
|
|
189
|
+
isStringLiteralLikeNode(node) &&
|
|
190
|
+
((isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) ||
|
|
191
|
+
patternOfKey(node) !== undefined);
|
|
192
|
+
|
|
193
|
+
const shorthandPropertySymbols = (project, node, names, unresolved) => {
|
|
194
|
+
if (isAssignmentTarget(assignmentKeyProperty(node).parent)) {
|
|
195
|
+
if (!names.candidates.has(node.text)) return [];
|
|
196
|
+
const property = assignmentPropertySymbol(project, node);
|
|
197
|
+
if (!property) unresolved.push(node);
|
|
198
|
+
return property ? [property] : [];
|
|
199
|
+
}
|
|
200
|
+
if (!isShorthandPropertyName(node)) return [];
|
|
201
|
+
if (!names.readsExports || !names.imports.has(node.text)) return [];
|
|
202
|
+
const value = project.checker.getShorthandAssignmentValueSymbol(node.parent);
|
|
203
|
+
return value ? [value] : [];
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Returns the symbols that each name in `nodes` reads. `symbols` holds the
|
|
208
|
+
* checker symbol of each name. The key of a binding element, also a string or
|
|
209
|
+
* computed string key, reads a property of the destructured type. A key in an
|
|
210
|
+
* assignment pattern does the same. A shorthand property in an object literal
|
|
211
|
+
* also reads the binding it names, which can be an import.
|
|
212
|
+
*
|
|
213
|
+
* The candidate names of `state` limit the property lookups, and its export
|
|
214
|
+
* names enable the import lookups. `unresolved` lists the assignment pattern
|
|
215
|
+
* names whose source type is unknown.
|
|
216
|
+
*/
|
|
217
|
+
export const shorthandReferenceSymbols = (project, nodes, symbols, state) => {
|
|
218
|
+
const symbolsByNode = new Map();
|
|
219
|
+
const unresolved = [];
|
|
220
|
+
bindingPropertySymbols(
|
|
221
|
+
project,
|
|
222
|
+
nodes.filter(
|
|
223
|
+
(node) =>
|
|
224
|
+
state.candidateNames.has(node.text) && bindingKeyPattern(node) && !isRenamingKey(node),
|
|
225
|
+
),
|
|
226
|
+
symbolsByNode,
|
|
227
|
+
);
|
|
228
|
+
const shorthandProperties = nodes.filter(isPropertyName);
|
|
229
|
+
const readsExports = state.exportedNames.size > 0;
|
|
230
|
+
const names = {
|
|
231
|
+
candidates: state.candidateNames,
|
|
232
|
+
readsExports,
|
|
233
|
+
imports: readsExports && shorthandProperties.length > 0 ? importBindingNames(nodes) : new Set(),
|
|
234
|
+
};
|
|
235
|
+
shorthandProperties.forEach((node) => {
|
|
236
|
+
const extra = shorthandPropertySymbols(project, node, names, unresolved);
|
|
237
|
+
if (extra.length > 0) symbolsByNode.set(node, extra);
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
symbols: nodes.map((node, index) => [symbols[index], ...(symbolsByNode.get(node) ?? [])]),
|
|
241
|
+
unresolved,
|
|
242
|
+
};
|
|
243
|
+
};
|
package/src/symbol-impact.mjs
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
isCallExpression,
|
|
5
|
-
isExportDeclaration,
|
|
6
|
-
isImportDeclaration,
|
|
7
|
-
isStringLiteralLikeNode,
|
|
8
|
-
} from "typescript/unstable/ast/is";
|
|
3
|
+
import { ast } from "./backend.mjs";
|
|
9
4
|
|
|
10
5
|
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
11
6
|
import {
|
|
@@ -15,6 +10,8 @@ import {
|
|
|
15
10
|
sourceFileIdentity,
|
|
16
11
|
} from "./semantic-identity.mjs";
|
|
17
12
|
|
|
13
|
+
const { isCallExpression, isExportDeclaration, isImportDeclaration, isStringLiteralLikeNode } = ast;
|
|
14
|
+
|
|
18
15
|
const TEST_FILE_PATTERN = /(?:^|[/_.-])(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
19
16
|
const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
20
17
|
const slash = (value) => value.split(path.sep).join("/");
|