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.
@@ -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
- } from "typescript/unstable/ast/is";
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
- const moduleBodyNames = (body) => {
32
- if (body && isModuleDeclaration(body)) return moduleBodyNames(body.body);
33
- return body && isModuleBlock(body) ? body.statements.flatMap(statementAliasNames) : [];
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)) return moduleBodyNames(statement.body);
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
- const programAliasNames = (project) =>
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 { SymbolFlags } from "typescript/unstable/sync";
4
- import {
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
- } from "typescript/unstable/ast/is";
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, readFileSync } from "node:fs";
2
+ import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
- import { API, SignatureKind, SymbolFlags } from "typescript/unstable/sync";
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 { matchingAliasEntries, matchingSymbolEntries, targetName } from "./symbol-targets.mjs";
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
- isElementNameNode(node) && candidateNameMatches(candidateNames, node.text);
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 parent = node.parent;
520
- if (!parent) return undefined;
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.checker.getSymbolAtLocation(node);
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 = referencedModuleIdentity(project, node);
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 recordSymbolUncertainty = (state, entry, reasonCode) => {
616
- state.uncertaintiesByQuery.get(entry.query.id).add(reasonCode);
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 recordStringDispatchedMemberAccess = (state, projectEntries, node) => {
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 recordDynamicImportUncertainty = (state, project, node) => {
654
- if (!isDynamicImportCall(node)) return;
655
- const specifier = node.arguments[0];
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
- ...matchingAliasEntries(project, rawSymbol, namespace, state),
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.expression) ??
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) => new API({ 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 = (root, { createApi = (cwd) => new API({ cwd }) } = {}) => {
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
+ };
@@ -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("/");