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.
@@ -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
+ };
@@ -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
- isExportDeclaration,
8
- isImportDeclaration,
9
- isNamedExports,
10
- isNamedImports,
11
- } from "typescript/unstable/ast/is";
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
- const normalized = slash(project.configFileName);
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.compilerOptions),
259
- config_document: configDocumentClosure(root, project.configFileName),
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 { version as typescriptVersion } from "typescript";
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: BACKEND_FAMILY,
31
- backend_version: 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: BACKEND_FAMILY,
495
- backend_version: 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: BACKEND_FAMILY,
511
- backend_version: 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
  });