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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { SyntaxKind, ast, sync } from "./backend.mjs";
|
|
2
|
+
import {
|
|
3
|
+
NODE_MODULES_SEGMENT,
|
|
4
|
+
isJavaScriptFile,
|
|
5
|
+
moduleBodyStatements,
|
|
6
|
+
ownSourceFiles,
|
|
7
|
+
} from "./reference-names.mjs";
|
|
8
|
+
import { declarationsForSymbol, resolveAlias, sourceFileIdentity } from "./semantic-identity.mjs";
|
|
9
|
+
import {
|
|
10
|
+
isAssignmentTarget,
|
|
11
|
+
patternOfKey,
|
|
12
|
+
patternSourceType,
|
|
13
|
+
qualifierExpression,
|
|
14
|
+
referenceValueSymbol,
|
|
15
|
+
} from "./shorthand-references.mjs";
|
|
16
|
+
import { matchingAliasEntries, recordSymbolUncertainty } from "./symbol-targets.mjs";
|
|
17
|
+
|
|
18
|
+
const { SymbolFlags } = sync;
|
|
19
|
+
const {
|
|
20
|
+
isAsExpression,
|
|
21
|
+
isBinaryExpression,
|
|
22
|
+
isBindingElement,
|
|
23
|
+
isCallExpression,
|
|
24
|
+
isComputedPropertyName,
|
|
25
|
+
isElementAccessExpression,
|
|
26
|
+
isExportDeclaration,
|
|
27
|
+
isIdentifier,
|
|
28
|
+
isImportClause,
|
|
29
|
+
isImportDeclaration,
|
|
30
|
+
isImportEqualsDeclaration,
|
|
31
|
+
isImportSpecifier,
|
|
32
|
+
isModuleDeclaration,
|
|
33
|
+
isNamedImports,
|
|
34
|
+
isNamespaceExport,
|
|
35
|
+
isNamespaceImport,
|
|
36
|
+
isNonNullExpression,
|
|
37
|
+
isNumericLiteral,
|
|
38
|
+
isObjectBindingPattern,
|
|
39
|
+
isObjectLiteralExpression,
|
|
40
|
+
isParameterDeclaration,
|
|
41
|
+
isParenthesizedExpression,
|
|
42
|
+
isPropertyAccessExpression,
|
|
43
|
+
isPropertyAssignment,
|
|
44
|
+
isQualifiedName,
|
|
45
|
+
isSatisfiesExpression,
|
|
46
|
+
isShorthandPropertyAssignment,
|
|
47
|
+
isStringLiteralLikeNode,
|
|
48
|
+
isTypeAssertion,
|
|
49
|
+
isTypeQueryNode,
|
|
50
|
+
isVariableDeclaration,
|
|
51
|
+
isVariableStatement,
|
|
52
|
+
} = ast;
|
|
53
|
+
|
|
54
|
+
// A namespace object holds every export of a module. A static member read of
|
|
55
|
+
// the object reads one export. Any other use, such as a call argument, an
|
|
56
|
+
// alias, a rest element, or a computed key, can read every export, so the
|
|
57
|
+
// exports of that module get an uncertainty instead of a closed-world claim.
|
|
58
|
+
|
|
59
|
+
const WRAPPERS = [
|
|
60
|
+
isParenthesizedExpression,
|
|
61
|
+
isAsExpression,
|
|
62
|
+
isNonNullExpression,
|
|
63
|
+
isSatisfiesExpression,
|
|
64
|
+
isTypeAssertion,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const isWrapper = (node) => WRAPPERS.some((matches) => matches(node));
|
|
68
|
+
|
|
69
|
+
const outermostWrapper = (node) => {
|
|
70
|
+
let current = node;
|
|
71
|
+
while (current.parent && isWrapper(current.parent) && current.parent.expression === current) {
|
|
72
|
+
current = current.parent;
|
|
73
|
+
}
|
|
74
|
+
return current;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const innermostExpression = (node) => {
|
|
78
|
+
let current = node;
|
|
79
|
+
while (current && isWrapper(current)) current = current.expression;
|
|
80
|
+
return current;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isLiteralKeyText = (node) => isStringLiteralLikeNode(node) || isNumericLiteral(node);
|
|
84
|
+
|
|
85
|
+
const isLiteralKey = (name) =>
|
|
86
|
+
isIdentifier(name) ||
|
|
87
|
+
isLiteralKeyText(name) ||
|
|
88
|
+
(isComputedPropertyName(name) && isLiteralKeyText(name.expression));
|
|
89
|
+
|
|
90
|
+
const isStaticBindingPattern = (pattern) =>
|
|
91
|
+
isObjectBindingPattern(pattern) &&
|
|
92
|
+
pattern.elements.every(
|
|
93
|
+
(element) =>
|
|
94
|
+
!element.dotDotDotToken && (!element.propertyName || isLiteralKey(element.propertyName)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const isStaticAssignmentPattern = (literal) =>
|
|
98
|
+
isObjectLiteralExpression(literal) &&
|
|
99
|
+
literal.properties.every(
|
|
100
|
+
(property) =>
|
|
101
|
+
isShorthandPropertyAssignment(property) ||
|
|
102
|
+
(isPropertyAssignment(property) && isLiteralKey(property.name)),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// The source is the initializer of a variable, or the default value of a
|
|
106
|
+
// parameter. A declared type replaces the type of the source, and then a key
|
|
107
|
+
// no longer resolves to the export.
|
|
108
|
+
const isStaticDeclarationSource = (parent, node) =>
|
|
109
|
+
parent.initializer === node && !parent.type && isStaticBindingPattern(parent.name);
|
|
110
|
+
|
|
111
|
+
const keyText = (name) => (isComputedPropertyName(name) ? name.expression.text : name.text);
|
|
112
|
+
|
|
113
|
+
const bindingKeys = (pattern) =>
|
|
114
|
+
pattern.elements.map((element) => keyText(element.propertyName ?? element.name));
|
|
115
|
+
|
|
116
|
+
const assignmentKeys = (literal) => literal.properties.map((property) => keyText(property.name));
|
|
117
|
+
|
|
118
|
+
const isStaticPatternType = (query) =>
|
|
119
|
+
(isParameterDeclaration(query.parent) || isVariableDeclaration(query.parent)) &&
|
|
120
|
+
query.parent.type === query &&
|
|
121
|
+
isStaticBindingPattern(query.parent.name);
|
|
122
|
+
|
|
123
|
+
// A member read names one export, and the scan tracks a namespace that this
|
|
124
|
+
// export holds by its name. A type position cannot read a value: a value of
|
|
125
|
+
// type `typeof ns` comes from a value use of `ns`, which the scan classifies on
|
|
126
|
+
// its own. Only a type that a pattern destructures reads keys.
|
|
127
|
+
const MEMBER_USE_RULES = [
|
|
128
|
+
[isPropertyAccessExpression, (parent, node) => parent.expression === node],
|
|
129
|
+
[isQualifiedName, (parent, node) => parent.left === node],
|
|
130
|
+
[isTypeQueryNode, (parent, node) => parent.exprName === node && !isStaticPatternType(parent)],
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const isStaticAssignmentSource = (parent, node) =>
|
|
134
|
+
parent.operatorToken.kind === SyntaxKind.EqualsToken &&
|
|
135
|
+
parent.right === node &&
|
|
136
|
+
!isAssignmentTarget(parent) &&
|
|
137
|
+
isStaticAssignmentPattern(parent.left);
|
|
138
|
+
|
|
139
|
+
const declarationKeys = (parent, node) =>
|
|
140
|
+
isStaticDeclarationSource(parent, node) ? bindingKeys(parent.name) : undefined;
|
|
141
|
+
|
|
142
|
+
// A keyed read, such as a literal element access or a destructure with literal
|
|
143
|
+
// keys, gives the export names that it reads. Such a read can pass on a nested
|
|
144
|
+
// namespace under another name.
|
|
145
|
+
const KEYED_USE_RULES = [
|
|
146
|
+
[
|
|
147
|
+
isElementAccessExpression,
|
|
148
|
+
(parent, node) =>
|
|
149
|
+
parent.expression === node && isLiteralKeyText(parent.argumentExpression)
|
|
150
|
+
? [parent.argumentExpression.text]
|
|
151
|
+
: undefined,
|
|
152
|
+
],
|
|
153
|
+
[isVariableDeclaration, declarationKeys],
|
|
154
|
+
[isParameterDeclaration, declarationKeys],
|
|
155
|
+
[
|
|
156
|
+
isBinaryExpression,
|
|
157
|
+
(parent, node) =>
|
|
158
|
+
isStaticAssignmentSource(parent, node) ? assignmentKeys(parent.left) : undefined,
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
isTypeQueryNode,
|
|
162
|
+
(parent, node) =>
|
|
163
|
+
parent.exprName === node && isStaticPatternType(parent)
|
|
164
|
+
? bindingKeys(parent.parent.name)
|
|
165
|
+
: undefined,
|
|
166
|
+
],
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const ruleResult = (rules, expression) => {
|
|
170
|
+
const parent = expression.parent;
|
|
171
|
+
const rule = parent && rules.find(([matches]) => matches(parent));
|
|
172
|
+
return rule ? rule[1](parent, expression) : undefined;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const IMPORT_DECLARATION_PARENTS = [
|
|
176
|
+
isNamespaceImport,
|
|
177
|
+
isNamespaceExport,
|
|
178
|
+
isImportEqualsDeclaration,
|
|
179
|
+
isImportClause,
|
|
180
|
+
isImportSpecifier,
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const NAMED_DECLARATION_PARENTS = [isVariableDeclaration, isBindingElement, isParameterDeclaration];
|
|
184
|
+
|
|
185
|
+
// The scan classifies each use of a binding, so its declaration is no use.
|
|
186
|
+
const isBindingDeclarationName = (node) =>
|
|
187
|
+
IMPORT_DECLARATION_PARENTS.some((matches) => matches(node.parent)) ||
|
|
188
|
+
NAMED_DECLARATION_PARENTS.some((matches) => matches(node.parent) && node.parent.name === node);
|
|
189
|
+
|
|
190
|
+
// `outer.inner` reads the namespace `inner` that a barrel re-exports.
|
|
191
|
+
const namespaceExpression = (node) => {
|
|
192
|
+
const parent = node.parent;
|
|
193
|
+
const isMemberName =
|
|
194
|
+
(isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
195
|
+
(isQualifiedName(parent) && parent.right === node);
|
|
196
|
+
return outermostWrapper(isMemberName ? parent : node);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const isRequireCall = (node) =>
|
|
200
|
+
node !== undefined &&
|
|
201
|
+
isCallExpression(node) &&
|
|
202
|
+
isIdentifier(node.expression) &&
|
|
203
|
+
node.expression.text === "require";
|
|
204
|
+
|
|
205
|
+
const namespaceImportNames = (statement) => {
|
|
206
|
+
const bindings = statement.importClause?.namedBindings;
|
|
207
|
+
return bindings && isNamespaceImport(bindings) ? [bindings.name] : [];
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const namespaceExportNames = (statement) =>
|
|
211
|
+
statement.exportClause && isNamespaceExport(statement.exportClause)
|
|
212
|
+
? [statement.exportClause.name]
|
|
213
|
+
: [];
|
|
214
|
+
|
|
215
|
+
const requireBindingNames = (statement) =>
|
|
216
|
+
statement.declarationList.declarations
|
|
217
|
+
.filter((declaration) => isRequireCall(declaration.initializer))
|
|
218
|
+
.map((declaration) => declaration.name);
|
|
219
|
+
|
|
220
|
+
const NAMESPACE_NAME_RULES = [
|
|
221
|
+
[isImportDeclaration, namespaceImportNames],
|
|
222
|
+
[isImportEqualsDeclaration, (statement) => [statement.name]],
|
|
223
|
+
[isExportDeclaration, namespaceExportNames],
|
|
224
|
+
[isVariableStatement, requireBindingNames],
|
|
225
|
+
[
|
|
226
|
+
isModuleDeclaration,
|
|
227
|
+
(statement) => moduleBodyStatements(statement.body).flatMap(statementNamespaceNames),
|
|
228
|
+
],
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const statementNamespaceNames = (statement) =>
|
|
232
|
+
NAMESPACE_NAME_RULES.find(([matches]) => matches(statement))?.[1](statement) ?? [];
|
|
233
|
+
|
|
234
|
+
// Each binding is a pair of the imported name and the local name. A default
|
|
235
|
+
// import reads `default`, which `export * as default` can make a namespace.
|
|
236
|
+
const importBindings = (statement) => {
|
|
237
|
+
const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
|
|
238
|
+
const bindings = clause?.namedBindings;
|
|
239
|
+
const named =
|
|
240
|
+
bindings && isNamedImports(bindings)
|
|
241
|
+
? bindings.elements.map((element) => [
|
|
242
|
+
(element.propertyName ?? element.name).text,
|
|
243
|
+
element.name.text,
|
|
244
|
+
])
|
|
245
|
+
: [];
|
|
246
|
+
return clause?.name ? [["default", clause.name.text], ...named] : named;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// `import { inner as local }` binds a namespace that a barrel re-exports under
|
|
250
|
+
// a new name. Each pass adds one level of renames.
|
|
251
|
+
const addRenamedImports = (names, bindings) => {
|
|
252
|
+
let size = -1;
|
|
253
|
+
while (size !== names.size) {
|
|
254
|
+
size = names.size;
|
|
255
|
+
bindings.filter(([imported]) => names.has(imported)).forEach(([, local]) => names.add(local));
|
|
256
|
+
}
|
|
257
|
+
return names;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Only these declarations bind a name to a namespace object. The name check
|
|
261
|
+
// skips the alias lookup, which costs checker round trips, for other names.
|
|
262
|
+
const programNamespaceNames = (project) => {
|
|
263
|
+
const statements = ownSourceFiles(project).flatMap((sourceFile) => sourceFile.statements);
|
|
264
|
+
const names = statements
|
|
265
|
+
.flatMap(statementNamespaceNames)
|
|
266
|
+
.filter((name) => name && isIdentifier(name))
|
|
267
|
+
.map((name) => name.text);
|
|
268
|
+
return addRenamedImports(new Set(names), statements.flatMap(importBindings));
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const caches = new WeakMap();
|
|
272
|
+
|
|
273
|
+
const batchCache = (state, project) => {
|
|
274
|
+
const byProject = caches.get(state) ?? new Map();
|
|
275
|
+
caches.set(state, byProject);
|
|
276
|
+
if (!byProject.has(project)) {
|
|
277
|
+
byProject.set(project, {
|
|
278
|
+
names: programNamespaceNames(project),
|
|
279
|
+
modulesBySymbol: new Map(),
|
|
280
|
+
nestedModules: new Map(),
|
|
281
|
+
escapedModules: new Set(),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return byProject.get(project);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const moduleSourceFile = (project, symbol) =>
|
|
288
|
+
symbol && (symbol.flags & SymbolFlags.ValueModule) !== 0
|
|
289
|
+
? declarationsForSymbol(project, symbol).find(
|
|
290
|
+
(declaration) => declaration.kind === SyntaxKind.SourceFile,
|
|
291
|
+
)
|
|
292
|
+
: undefined;
|
|
293
|
+
|
|
294
|
+
// A module below node_modules holds no query targets, so its namespace object
|
|
295
|
+
// needs no uncertainty.
|
|
296
|
+
const isOwnModule = (sourceFile) =>
|
|
297
|
+
sourceFile !== undefined && !NODE_MODULES_SEGMENT.test(sourceFile.fileName);
|
|
298
|
+
|
|
299
|
+
const namespaceModule = (project, cache, symbol) => {
|
|
300
|
+
if (!symbol || (symbol.flags & SymbolFlags.Alias) === 0) return undefined;
|
|
301
|
+
if (!cache.modulesBySymbol.has(symbol)) {
|
|
302
|
+
const target = resolveAlias(project.checker, symbol);
|
|
303
|
+
cache.modulesBySymbol.set(
|
|
304
|
+
symbol,
|
|
305
|
+
isOwnModule(moduleSourceFile(project, target)) ? target : undefined,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return cache.modulesBySymbol.get(symbol);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Only an alias or a symbol with a queried export name can match an entry.
|
|
312
|
+
const canMatchEntry = (state, symbol) =>
|
|
313
|
+
(symbol.flags & SymbolFlags.Alias) !== 0 || state.exportedNames.has(symbol.name);
|
|
314
|
+
|
|
315
|
+
const recordModuleEscape = (state, project, cache, moduleSymbol) => {
|
|
316
|
+
if (cache.escapedModules.has(moduleSymbol)) return;
|
|
317
|
+
cache.escapedModules.add(moduleSymbol);
|
|
318
|
+
const identity = sourceFileIdentity(moduleSourceFile(project, moduleSymbol));
|
|
319
|
+
const entries = new Set(state.exportEntriesByModule.get(identity) ?? []);
|
|
320
|
+
const exports = project.checker.getExportsOfModule(moduleSymbol);
|
|
321
|
+
for (const exported of exports.filter((symbol) => canMatchEntry(state, symbol))) {
|
|
322
|
+
matchingAliasEntries(project, exported, "value", state).forEach((entry) => entries.add(entry));
|
|
323
|
+
const reexported = namespaceModule(project, cache, exported);
|
|
324
|
+
if (reexported) recordModuleEscape(state, project, cache, reexported);
|
|
325
|
+
}
|
|
326
|
+
entries.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-behavior"));
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const nestedNamespaceModules = (project, cache, moduleSymbol) => {
|
|
330
|
+
if (!cache.nestedModules.has(moduleSymbol)) {
|
|
331
|
+
const nested = project.checker
|
|
332
|
+
.getExportsOfModule(moduleSymbol)
|
|
333
|
+
.map((exported) => ({
|
|
334
|
+
name: exported.name,
|
|
335
|
+
module: namespaceModule(project, cache, exported),
|
|
336
|
+
}))
|
|
337
|
+
.filter(({ module }) => module);
|
|
338
|
+
cache.nestedModules.set(moduleSymbol, nested);
|
|
339
|
+
}
|
|
340
|
+
return cache.nestedModules.get(moduleSymbol);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const recordExpressionUse = (state, project, cache, expression, moduleSymbol) => {
|
|
344
|
+
if (ruleResult(MEMBER_USE_RULES, expression)) return;
|
|
345
|
+
const keys = ruleResult(KEYED_USE_RULES, expression);
|
|
346
|
+
const escaped = keys
|
|
347
|
+
? nestedNamespaceModules(project, cache, moduleSymbol)
|
|
348
|
+
.filter(({ name }) => keys.includes(name))
|
|
349
|
+
.map(({ module }) => module)
|
|
350
|
+
: [moduleSymbol];
|
|
351
|
+
escaped.forEach((escapedModule) => recordModuleEscape(state, project, cache, escapedModule));
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// The checker gives no symbol for the specifier of `module.exports =
|
|
355
|
+
// require(...)`, but the call has the module type.
|
|
356
|
+
const requiredModule = (project, call) => {
|
|
357
|
+
const symbol = project.checker.getSymbolAtLocation(call.arguments[0]);
|
|
358
|
+
return moduleSourceFile(project, symbol)
|
|
359
|
+
? symbol
|
|
360
|
+
: project.checker.getTypeAtLocation(call)?.getSymbol();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const requireCallOf = (node) => {
|
|
364
|
+
const call = node.parent;
|
|
365
|
+
return isRequireCall(call) && call.expression === node ? call : undefined;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const hasLiteralSpecifier = (call) =>
|
|
369
|
+
call.arguments.length === 1 && isStringLiteralLikeNode(call.arguments[0]);
|
|
370
|
+
|
|
371
|
+
const initializesTrackedName = (cache, expression) => {
|
|
372
|
+
const declaration = expression.parent;
|
|
373
|
+
return (
|
|
374
|
+
isVariableDeclaration(declaration) &&
|
|
375
|
+
declaration.initializer === expression &&
|
|
376
|
+
isIdentifier(declaration.name) &&
|
|
377
|
+
cache.names.has(declaration.name.text)
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// A `require` call that does not initialize a tracked name, such as
|
|
382
|
+
// `module.exports = require("./values")`, is a namespace object use.
|
|
383
|
+
const recordRequireUse = (state, project, cache, node) => {
|
|
384
|
+
const call = requireCallOf(node);
|
|
385
|
+
if (!call || !hasLiteralSpecifier(call)) return;
|
|
386
|
+
const expression = outermostWrapper(call);
|
|
387
|
+
if (initializesTrackedName(cache, expression)) return;
|
|
388
|
+
const moduleSymbol = requiredModule(project, call);
|
|
389
|
+
if (isOwnModule(moduleSourceFile(project, moduleSymbol))) {
|
|
390
|
+
recordExpressionUse(state, project, cache, expression, moduleSymbol);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Records a `dynamic-behavior` uncertainty for the exports that a namespace
|
|
396
|
+
* object use can read, unless the use is a static member read.
|
|
397
|
+
*/
|
|
398
|
+
export const recordNamespaceUse = (state, project, node, symbol) => {
|
|
399
|
+
if (state.exportedNames.size === 0 || !isIdentifier(node)) return;
|
|
400
|
+
const cache = batchCache(state, project);
|
|
401
|
+
if (node.text === "require" && isJavaScriptFile(node.getSourceFile())) {
|
|
402
|
+
recordRequireUse(state, project, cache, node);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!cache.names.has(node.text) || isBindingDeclarationName(node)) return;
|
|
406
|
+
const valueSymbol = isShorthandPropertyAssignment(node.parent)
|
|
407
|
+
? referenceValueSymbol(project, node)
|
|
408
|
+
: symbol;
|
|
409
|
+
const moduleSymbol = namespaceModule(project, cache, valueSymbol);
|
|
410
|
+
if (moduleSymbol) {
|
|
411
|
+
recordExpressionUse(state, project, cache, namespaceExpression(node), moduleSymbol);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const recordImportedModuleEscape = (state, project, specifier) => {
|
|
416
|
+
const moduleSymbol = project.checker.getSymbolAtLocation(specifier);
|
|
417
|
+
if (isOwnModule(moduleSourceFile(project, moduleSymbol))) {
|
|
418
|
+
recordModuleEscape(state, project, batchCache(state, project), moduleSymbol);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const isDynamicImportCall = (node) =>
|
|
423
|
+
isCallExpression(node) && node.expression.getText(node.getSourceFile()) === "import";
|
|
424
|
+
|
|
425
|
+
const recordModuleEntriesUncertainty = (state, entries) =>
|
|
426
|
+
(entries ?? []).forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-behavior"));
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Records a `dynamic-behavior` uncertainty for the exports that a dynamic
|
|
430
|
+
* import can read. A non-literal specifier can load any module of the project.
|
|
431
|
+
* A literal specifier loads one namespace object, which also holds the exports
|
|
432
|
+
* that the module re-exports.
|
|
433
|
+
*/
|
|
434
|
+
export const recordDynamicImportUncertainty = (state, project, node, moduleIdentityOf) => {
|
|
435
|
+
if (!isDynamicImportCall(node)) return;
|
|
436
|
+
const specifier = node.arguments[0];
|
|
437
|
+
if (!specifier || !isStringLiteralLikeNode(specifier)) {
|
|
438
|
+
recordModuleEntriesUncertainty(state, state.exportEntriesByProject.get(project));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (state.exportedNames.size > 0) recordImportedModuleEscape(state, project, specifier);
|
|
442
|
+
const moduleIdentity = moduleIdentityOf(project, specifier);
|
|
443
|
+
if (moduleIdentity) {
|
|
444
|
+
recordModuleEntriesUncertainty(state, state.exportEntriesByModule.get(moduleIdentity));
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const moduleIdentityOfSymbol = (project, symbol) => {
|
|
449
|
+
const sourceFile = moduleSourceFile(project, symbol);
|
|
450
|
+
return sourceFile ? sourceFileIdentity(sourceFile) : undefined;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const moduleIdentityOfType = (project, type) =>
|
|
454
|
+
type ? moduleIdentityOfSymbol(project, type.getSymbol()) : undefined;
|
|
455
|
+
|
|
456
|
+
// Many reads share one qualifier symbol, such as `ns` in `(ns as T).name`.
|
|
457
|
+
const symbolModuleIdentities = new WeakMap();
|
|
458
|
+
|
|
459
|
+
const aliasModuleIdentity = (project, symbol) => {
|
|
460
|
+
if (!symbol) return undefined;
|
|
461
|
+
const bySymbol = symbolModuleIdentities.get(project) ?? new Map();
|
|
462
|
+
symbolModuleIdentities.set(project, bySymbol);
|
|
463
|
+
if (!bySymbol.has(symbol)) {
|
|
464
|
+
bySymbol.set(symbol, moduleIdentityOfSymbol(project, resolveAlias(project.checker, symbol)));
|
|
465
|
+
}
|
|
466
|
+
return bySymbol.get(symbol);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const qualifierModuleIdentity = (project, qualifier) => {
|
|
470
|
+
const symbol = project.checker.getSymbolAtLocation(innermostExpression(qualifier));
|
|
471
|
+
return (
|
|
472
|
+
aliasModuleIdentity(project, symbol) ??
|
|
473
|
+
(isQualifiedName(qualifier)
|
|
474
|
+
? undefined
|
|
475
|
+
: moduleIdentityOfType(project, project.checker.getTypeAtLocation(qualifier)))
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const sourceIdentityOf = (project, node) => {
|
|
480
|
+
const pattern = patternOfKey(node);
|
|
481
|
+
if (pattern) return moduleIdentityOfType(project, patternSourceType(project, pattern));
|
|
482
|
+
const qualifier = qualifierExpression(node);
|
|
483
|
+
return qualifier ? qualifierModuleIdentity(project, qualifier) : undefined;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// The scan asks once for each entry that a name matches. One answer for each
|
|
487
|
+
// name keeps the checker round trips per reference, not per entry.
|
|
488
|
+
const sourceIdentities = new WeakMap();
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Returns the module whose namespace object a member name reads, such as the
|
|
492
|
+
* module of `ns` in `(ns as typeof ns).name` or in `({ name } = ns)`.
|
|
493
|
+
*/
|
|
494
|
+
export const namespaceSourceIdentity = (project, node) => {
|
|
495
|
+
const byNode = sourceIdentities.get(project) ?? new WeakMap();
|
|
496
|
+
sourceIdentities.set(project, byNode);
|
|
497
|
+
if (!byNode.has(node)) byNode.set(node, sourceIdentityOf(project, node));
|
|
498
|
+
return byNode.get(node);
|
|
499
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { projectCompilerOptions, projectConfigFileName } from "./backend.mjs";
|
|
5
|
+
|
|
6
|
+
const pathWithin = (root, candidate) =>
|
|
7
|
+
candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
8
|
+
|
|
9
|
+
const packageAt = (directory) => {
|
|
10
|
+
const packageFile = path.join(directory, "package.json");
|
|
11
|
+
if (!existsSync(packageFile)) return undefined;
|
|
12
|
+
try {
|
|
13
|
+
return { root: directory, json: JSON.parse(readFileSync(packageFile, "utf8")) };
|
|
14
|
+
} catch {
|
|
15
|
+
return { root: directory, json: {} };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** The nearest package.json at or above the project config, inside `root`. */
|
|
20
|
+
export const projectPackage = (root, project) => {
|
|
21
|
+
const normalizedRoot = path.resolve(root);
|
|
22
|
+
let current = path.dirname(projectConfigFileName(project));
|
|
23
|
+
while (pathWithin(normalizedRoot, current)) {
|
|
24
|
+
const found = packageAt(current);
|
|
25
|
+
if (found) return found;
|
|
26
|
+
if (current === normalizedRoot) break;
|
|
27
|
+
current = path.dirname(current);
|
|
28
|
+
}
|
|
29
|
+
return { root: normalizedRoot, json: {} };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The `compilerOptions.paths` targets that map the package name. */
|
|
33
|
+
export const packagePathTargets = (project, packageJson) => {
|
|
34
|
+
const packageName = packageJson.name;
|
|
35
|
+
if (typeof packageName !== "string") return [];
|
|
36
|
+
return projectCompilerOptions(project).paths?.[packageName] ?? [];
|
|
37
|
+
};
|
package/src/project-state.mjs
CHANGED
|
@@ -2,17 +2,20 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { SymbolFlags } from "typescript/unstable/sync";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
ast,
|
|
7
|
+
isInferredProject,
|
|
8
|
+
projectCompilerOptions,
|
|
9
|
+
projectConfigFileName,
|
|
10
|
+
sync,
|
|
11
|
+
} from "./backend.mjs";
|
|
12
12
|
|
|
13
13
|
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
14
14
|
import { projectSourceFiles, relativePath } from "./semantic-identity.mjs";
|
|
15
15
|
|
|
16
|
+
const { SymbolFlags } = sync;
|
|
17
|
+
const { isExportDeclaration, isImportDeclaration, isNamedExports, isNamedImports } = ast;
|
|
18
|
+
|
|
16
19
|
const INFERRED_PROJECT = "<inferred>";
|
|
17
20
|
const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
18
21
|
const slash = (value) => value.split(path.sep).join("/");
|
|
@@ -105,8 +108,7 @@ const hasSvelteVirtualModuleGap = (project) =>
|
|
|
105
108
|
);
|
|
106
109
|
|
|
107
110
|
const configPath = (root, project) => {
|
|
108
|
-
|
|
109
|
-
if (normalized.endsWith("/dev/null/inferred")) return INFERRED_PROJECT;
|
|
111
|
+
if (isInferredProject(project)) return INFERRED_PROJECT;
|
|
110
112
|
return relativePath(root, project.configFileName) || path.basename(project.configFileName);
|
|
111
113
|
};
|
|
112
114
|
|
|
@@ -255,8 +257,8 @@ const configDocumentClosure = (root, configFileName, seen = new Set()) => {
|
|
|
255
257
|
const effectiveProjectConfigHash = (root, project) => {
|
|
256
258
|
const effective = {
|
|
257
259
|
config: configPath(root, project),
|
|
258
|
-
compiler_options: normalizedConfigValue(root, project
|
|
259
|
-
config_document: configDocumentClosure(root, project
|
|
260
|
+
compiler_options: normalizedConfigValue(root, projectCompilerOptions(project)),
|
|
261
|
+
config_document: configDocumentClosure(root, projectConfigFileName(project)),
|
|
260
262
|
};
|
|
261
263
|
return `sha256:${createHash("sha256").update(JSON.stringify(effective)).digest("hex")}`;
|
|
262
264
|
};
|
package/src/protocol.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { BACKEND } from "./backend.mjs";
|
|
6
5
|
import {
|
|
7
6
|
ANALYSIS_OPERATION,
|
|
8
|
-
BACKEND_FAMILY,
|
|
9
|
-
BACKEND_VERSION,
|
|
10
7
|
MUTANT_TYPECHECK_OPERATION,
|
|
11
8
|
QUERY_OPERATIONS as MANIFEST_QUERY_OPERATIONS,
|
|
12
9
|
WIRE_PROTOCOL_VERSION,
|
|
@@ -18,17 +15,11 @@ const require = createRequire(import.meta.url);
|
|
|
18
15
|
const { version: SIDECAR_VERSION } = require("../package.json");
|
|
19
16
|
const QUERY_OPERATIONS = new Set(MANIFEST_QUERY_OPERATIONS);
|
|
20
17
|
|
|
21
|
-
if (typescriptVersion !== BACKEND_VERSION) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`typescript backend version ${typescriptVersion} does not match protocol ${BACKEND_VERSION}`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
18
|
export const createStatusResponse = () => ({
|
|
28
19
|
package_version: SIDECAR_VERSION,
|
|
29
20
|
protocol_version: WIRE_PROTOCOL_VERSION,
|
|
30
|
-
backend_family:
|
|
31
|
-
backend_version:
|
|
21
|
+
backend_family: BACKEND.family,
|
|
22
|
+
backend_version: BACKEND.version,
|
|
32
23
|
});
|
|
33
24
|
|
|
34
25
|
const BATCH_REQUEST_KEYS = new Set([
|
|
@@ -491,8 +482,8 @@ export const createSemanticResponse = ({
|
|
|
491
482
|
protocol_version: WIRE_PROTOCOL_VERSION,
|
|
492
483
|
operation: ANALYSIS_OPERATION,
|
|
493
484
|
sidecar_version: SIDECAR_VERSION,
|
|
494
|
-
backend:
|
|
495
|
-
backend_version:
|
|
485
|
+
backend: BACKEND.family,
|
|
486
|
+
backend_version: BACKEND.version,
|
|
496
487
|
selected_tsconfigs: [...selectedTsconfigs].toSorted(compareText),
|
|
497
488
|
projects: [...projectResults].toSorted((left, right) => compareText(left.config, right.config)),
|
|
498
489
|
results: [...results]
|
|
@@ -507,8 +498,8 @@ export const createMutantTypecheckResponse = ({ results, elapsedMs }) => ({
|
|
|
507
498
|
protocol_version: WIRE_PROTOCOL_VERSION,
|
|
508
499
|
operation: MUTANT_TYPECHECK_OPERATION,
|
|
509
500
|
sidecar_version: SIDECAR_VERSION,
|
|
510
|
-
backend:
|
|
511
|
-
backend_version:
|
|
501
|
+
backend: BACKEND.family,
|
|
502
|
+
backend_version: BACKEND.version,
|
|
512
503
|
results,
|
|
513
504
|
elapsed_ms: Math.max(0, Math.round(elapsedMs)),
|
|
514
505
|
});
|