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.
- 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 +84 -0
- package/src/semantic-identity.mjs +7 -5
- package/src/semantic.mjs +94 -197
- package/src/shorthand-references.mjs +243 -0
- package/src/symbol-impact.mjs +3 -6
- package/src/symbol-targets.mjs +74 -0
|
@@ -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("/");
|
|
@@ -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
|
+
};
|