fallow-type-aware 3.34.1 → 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.
@@ -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("/");
@@ -0,0 +1,74 @@
1
+ import { sync } from "./backend.mjs";
2
+
3
+ import {
4
+ declarationName,
5
+ declarationNamespaces,
6
+ declarationsForSymbol,
7
+ stableDeclarationKey,
8
+ } from "./semantic-identity.mjs";
9
+
10
+ const { SymbolFlags } = sync;
11
+
12
+ export const targetName = (declaration) => declarationName(declaration) ?? "default";
13
+
14
+ // A target key contains the declaration name. The name check skips the key
15
+ // and its file identity for every declaration that cannot match.
16
+ const targetsForDeclaration = (state, declaration, namespace) =>
17
+ state.targetNames.has(targetName(declaration))
18
+ ? (state.targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [])
19
+ : [];
20
+
21
+ export const matchingSymbolEntries = (project, symbol, namespace, state) => {
22
+ const declarations = declarationsForSymbol(project, symbol);
23
+ const matchingLane = declarations.filter((declaration) =>
24
+ declarationNamespaces(declaration).has(namespace),
25
+ );
26
+ if (matchingLane.length > 0) {
27
+ return new Set(
28
+ matchingLane.flatMap((declaration) => targetsForDeclaration(state, declaration, namespace)),
29
+ );
30
+ }
31
+ return new Set(
32
+ declarations.flatMap((declaration) =>
33
+ [...declarationNamespaces(declaration)].flatMap((declaredNamespace) =>
34
+ targetsForDeclaration(state, declaration, declaredNamespace),
35
+ ),
36
+ ),
37
+ );
38
+ };
39
+
40
+ const resolveAliasEntries = (project, symbol, namespace, state) => {
41
+ const entries = new Set();
42
+ const seen = new Set();
43
+ let current = symbol;
44
+ while (current && !seen.has(current)) {
45
+ seen.add(current);
46
+ matchingSymbolEntries(project, current, namespace, state).forEach((entry) =>
47
+ entries.add(entry),
48
+ );
49
+ if ((current.flags & SymbolFlags.Alias) === 0) break;
50
+ const aliased = project.checker.getAliasedSymbol(current);
51
+ if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
52
+ current = aliased;
53
+ }
54
+ return entries;
55
+ };
56
+
57
+ // The scan visits every identifier, so one symbol occurs many times. Each
58
+ // checker call is a round trip, and the targets do not change during a batch,
59
+ // so the batch keeps one answer for each symbol and namespace.
60
+ export const matchingAliasEntries = (project, symbol, namespace, state) => {
61
+ if (!symbol) return new Set();
62
+ const bySymbol = state.aliasEntriesByProject.get(project) ?? new Map();
63
+ state.aliasEntriesByProject.set(project, bySymbol);
64
+ const byNamespace = bySymbol.get(symbol) ?? new Map();
65
+ bySymbol.set(symbol, byNamespace);
66
+ if (!byNamespace.has(namespace)) {
67
+ byNamespace.set(namespace, resolveAliasEntries(project, symbol, namespace, state));
68
+ }
69
+ return byNamespace.get(namespace);
70
+ };
71
+
72
+ export const recordSymbolUncertainty = (state, entry, reasonCode) => {
73
+ state.uncertaintiesByQuery.get(entry.query.id).add(reasonCode);
74
+ };