fallow-type-aware 3.34.1 → 3.35.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fallow-type-aware",
3
- "version": "3.34.1",
3
+ "version": "3.35.0",
4
4
  "description": "Optional TypeScript-Go semantic refinement sidecar for Fallow",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,75 @@
1
+ import {
2
+ isExportDeclaration,
3
+ isImportDeclaration,
4
+ isImportEqualsDeclaration,
5
+ isModuleBlock,
6
+ isModuleDeclaration,
7
+ isNamedExports,
8
+ isNamespaceExportDeclaration,
9
+ isNamespaceImport,
10
+ } from "typescript/unstable/ast/is";
11
+
12
+ const JAVASCRIPT_FILE = /\.[cm]?jsx?$/iu;
13
+
14
+ // In JavaScript, require bindings and export assignments also create aliases.
15
+ // The scan keeps every identifier of these files.
16
+ export const isJavaScriptFile = (sourceFile) => JAVASCRIPT_FILE.test(sourceFile.fileName);
17
+
18
+ const importNames = ({ importClause }) => {
19
+ const bindings = importClause?.namedBindings;
20
+ if (!bindings) return [importClause?.name];
21
+ if (isNamespaceImport(bindings)) return [importClause.name, bindings.name];
22
+ return [importClause.name, ...bindings.elements.map((element) => element.name)];
23
+ };
24
+
25
+ const exportNames = ({ exportClause }) => {
26
+ if (!exportClause) return [];
27
+ if (isNamedExports(exportClause)) return exportClause.elements.map((element) => element.name);
28
+ return [exportClause.name];
29
+ };
30
+
31
+ const moduleBodyNames = (body) => {
32
+ if (body && isModuleDeclaration(body)) return moduleBodyNames(body.body);
33
+ return body && isModuleBlock(body) ? body.statements.flatMap(statementAliasNames) : [];
34
+ };
35
+
36
+ const statementAliasNames = (statement) => {
37
+ if (isImportDeclaration(statement)) return importNames(statement);
38
+ if (isImportEqualsDeclaration(statement) || isNamespaceExportDeclaration(statement)) {
39
+ return [statement.name];
40
+ }
41
+ if (isExportDeclaration(statement)) return exportNames(statement);
42
+ if (isModuleDeclaration(statement)) return moduleBodyNames(statement.body);
43
+ return [];
44
+ };
45
+
46
+ // Default-library and external-library files sit below node_modules. The
47
+ // path check skips them before the source file and its metadata, which each
48
+ // cost a checker round trip.
49
+ const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/u;
50
+
51
+ const isOwnSourceFile = (project, sourceFile) =>
52
+ Boolean(sourceFile) &&
53
+ !project.program.isSourceFileDefaultLibrary(sourceFile) &&
54
+ !project.program.isSourceFileFromExternalLibrary(sourceFile);
55
+
56
+ const programAliasNames = (project) =>
57
+ project.program
58
+ .getSourceFileNames()
59
+ .filter((fileName) => !NODE_MODULES_SEGMENT.test(fileName))
60
+ .map((fileName) => project.program.getSourceFile(fileName))
61
+ .filter((sourceFile) => isOwnSourceFile(project, sourceFile))
62
+ .flatMap((sourceFile) => sourceFile.statements.flatMap(statementAliasNames))
63
+ .map((name) => name?.text)
64
+ .filter((text) => typeof text === "string");
65
+
66
+ /**
67
+ * Returns the identifier names that can reach a symbol-use target.
68
+ *
69
+ * An identifier resolves to a declaration with its own name, or to an alias
70
+ * that an import or export declares under that name. So the scan needs only
71
+ * the target names and the names that the alias declarations of the project
72
+ * files declare, project declaration files included.
73
+ */
74
+ export const referenceNames = (projects, targetNames) =>
75
+ new Set([...targetNames, "default", ...projects.flatMap(programAliasNames)]);
package/src/semantic.mjs CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  isGetAccessorDeclaration,
16
16
  isIdentifier,
17
17
  isImportDeclaration,
18
- isImportClause,
19
18
  isImportSpecifier,
20
19
  isImportTypeNode,
21
20
  isNamespaceImport,
@@ -51,6 +50,8 @@ import {
51
50
  symbolForDeclaration,
52
51
  } from "./semantic-identity.mjs";
53
52
  import { analyzeSymbolImpact } from "./symbol-impact.mjs";
53
+ import { isJavaScriptFile, referenceNames } from "./reference-names.mjs";
54
+ import { matchingAliasEntries, matchingSymbolEntries, targetName } from "./symbol-targets.mjs";
54
55
  import { analyzeTypeCoupling } from "./type-coupling.mjs";
55
56
 
56
57
  const ATTACHED_COMMENT_PATTERN = /\/\/|\/\*/u;
@@ -352,15 +353,10 @@ const semanticUse = (root, node, namespace) => {
352
353
  };
353
354
  };
354
355
 
355
- const isDefaultImportReference = (node) => isImportClause(node.parent) && node.parent.name === node;
356
-
357
356
  const candidateNameMatches = (candidateNames, name) => !candidateNames || candidateNames.has(name);
358
357
 
359
- const isIdentifierReference = (node, candidateNames, includeDefaultImports) => {
360
- if (!isIdentifier(node)) return false;
361
- if (candidateNameMatches(candidateNames, node.text)) return true;
362
- return includeDefaultImports && isDefaultImportReference(node);
363
- };
358
+ const isIdentifierReference = (node, names) =>
359
+ isIdentifier(node) && candidateNameMatches(names, node.text);
364
360
 
365
361
  const isElementNameNode = (node) =>
366
362
  isStringLiteralLikeNode(node) &&
@@ -396,8 +392,11 @@ const symbolUseBatchState = (resolvedQueries, evidenceLimit) => {
396
392
  uncertaintiesByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
397
393
  declarationLocationsByQuery: new Map(resolvedQueries.map(({ query }) => [query.id, new Set()])),
398
394
  targetsByKey: new Map(),
395
+ targetNames: new Set(),
396
+ aliasEntriesByProject: new Map(),
399
397
  exportEntriesByModule: new Map(),
400
398
  exportEntriesByProject: new Map(),
399
+ exportedNames: new Set(),
401
400
  candidateNames: new Set(),
402
401
  classMemberEntries,
403
402
  classMemberEntriesByName: entriesByName(classMemberEntries),
@@ -412,6 +411,7 @@ const addSymbolTarget = (root, state, entry, declaration, namespace) => {
412
411
  state.declarationLocationsByQuery
413
412
  .get(entry.query.id)
414
413
  .add(locationKey(location(root, declaration.name ?? declaration)));
414
+ state.targetNames.add(targetName(declaration));
415
415
  const key = stableDeclarationKey(declaration, namespace);
416
416
  const targets = state.targetsByKey.get(key) ?? [];
417
417
  targets.push(entry);
@@ -422,6 +422,7 @@ const registerSymbolTargets = (root, state, entry) => {
422
422
  state.candidateNames.add(entry.query.symbol.localName);
423
423
  state.candidateNames.add(entry.query.symbol.exportedName);
424
424
  if (entry.query.symbol.declarationKind === "export") {
425
+ state.exportedNames.add(entry.query.symbol.exportedName);
425
426
  const moduleIdentity = canonicalFileIdentity(entry.query.symbol.absolutePath);
426
427
  const entries = state.exportEntriesByModule.get(moduleIdentity) ?? new Set();
427
428
  entries.add(entry);
@@ -447,45 +448,6 @@ const registerSymbolTargets = (root, state, entry) => {
447
448
  state.contractRelationsByQuery.set(entry.query.id, entry.resolved.contractRelations);
448
449
  };
449
450
 
450
- const matchingSymbolEntries = (project, symbol, namespace, targetsByKey) => {
451
- const declarations = declarationsForSymbol(project, symbol);
452
- const matchingLane = declarations.filter((declaration) =>
453
- declarationNamespaces(declaration).has(namespace),
454
- );
455
- if (matchingLane.length > 0) {
456
- return new Set(
457
- matchingLane.flatMap(
458
- (declaration) => targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [],
459
- ),
460
- );
461
- }
462
- return new Set(
463
- declarations.flatMap((declaration) =>
464
- [...declarationNamespaces(declaration)].flatMap(
465
- (declaredNamespace) =>
466
- targetsByKey.get(stableDeclarationKey(declaration, declaredNamespace)) ?? [],
467
- ),
468
- ),
469
- );
470
- };
471
-
472
- const matchingAliasEntries = (project, symbol, namespace, targetsByKey) => {
473
- const entries = new Set();
474
- const seen = new Set();
475
- let current = symbol;
476
- while (current && !seen.has(current)) {
477
- seen.add(current);
478
- matchingSymbolEntries(project, current, namespace, targetsByKey).forEach((entry) =>
479
- entries.add(entry),
480
- );
481
- if ((current.flags & SymbolFlags.Alias) === 0) break;
482
- const aliased = project.checker.getAliasedSymbol(current);
483
- if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
484
- current = aliased;
485
- }
486
- return entries;
487
- };
488
-
489
451
  const isDeclarationReference = (root, state, entry, node) => {
490
452
  const locations = state.declarationLocationsByQuery.get(entry.query.id);
491
453
  if (locations.has(locationKey(location(root, node)))) return true;
@@ -683,9 +645,7 @@ const recordComputedMemberAccess = (state, project, entries, node) => {
683
645
  const matched = new Set(
684
646
  receiverProperties
685
647
  .filter((property) => state.candidateNames.has(property.name))
686
- .flatMap((property) => [
687
- ...matchingSymbolEntries(project, property, "value", state.targetsByKey),
688
- ]),
648
+ .flatMap((property) => [...matchingSymbolEntries(project, property, "value", state)]),
689
649
  );
690
650
  matched.forEach((entry) => recordSymbolUncertainty(state, entry, "dynamic-member-access"));
691
651
  };
@@ -706,22 +666,24 @@ const recordDynamicImportUncertainty = (state, project, node) => {
706
666
  );
707
667
  };
708
668
 
709
- const referenceNamespaces = (project, node, symbol) => {
669
+ const referenceNamespaces = (project, node, rawSymbol) => {
710
670
  const alias = aliasNode(node);
711
671
  if (!alias || !isExportSpecifier(alias)) {
712
672
  return [isTypePosition(node) ? "type" : "value"];
713
673
  }
714
674
  if (alias.isTypeOnly || alias.parent?.parent?.isTypeOnly) return ["type"];
715
675
  const namespaces = new Set(
716
- declarationsForSymbol(project, symbol).flatMap((declaration) => [
717
- ...declarationNamespaces(declaration),
718
- ]),
676
+ declarationsForSymbol(project, resolveAlias(project.checker, rawSymbol)).flatMap(
677
+ (declaration) => [...declarationNamespaces(declaration)],
678
+ ),
719
679
  );
720
680
  return namespaces.size > 0 ? [...namespaces] : [isTypePosition(node) ? "type" : "value"];
721
681
  };
722
682
 
723
683
  const surfaceEntriesForNode = (state, project, node, namespace) => {
724
- if (moduleEdgeDeclaration(node)) return new Set();
684
+ // Resolving the referenced module costs checker round trips, so skip every
685
+ // identifier that cannot name an export query.
686
+ if (!state.exportedNames.has(node.text) || moduleEdgeDeclaration(node)) return new Set();
725
687
  const moduleIdentity = referencedModuleIdentity(project, node);
726
688
  if (!moduleIdentity) return new Set();
727
689
  const direct = [...(state.exportEntriesByModule.get(moduleIdentity) ?? [])].filter(
@@ -732,7 +694,7 @@ const surfaceEntriesForNode = (state, project, node, namespace) => {
732
694
  for (const entry of direct) {
733
695
  for (const { declaration } of entry.resolved.ownerContexts) {
734
696
  const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
735
- matchingSymbolEntries(project, symbol, namespace, state.targetsByKey).forEach((related) =>
697
+ matchingSymbolEntries(project, symbol, namespace, state).forEach((related) =>
736
698
  entries.add(related),
737
699
  );
738
700
  }
@@ -740,7 +702,7 @@ const surfaceEntriesForNode = (state, project, node, namespace) => {
740
702
  return entries;
741
703
  };
742
704
 
743
- const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImports) => {
705
+ const scanSymbolUseFile = (root, state, project, sourceFile, names) => {
744
706
  const projectEntries = classMemberEntrySetForProject(state, project);
745
707
  const nodes = [];
746
708
  visit(sourceFile, (node) => {
@@ -749,7 +711,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
749
711
  recordStringDispatchedMemberAccess(state, projectEntries, node);
750
712
  recordComputedMemberAccess(state, project, projectEntries, node);
751
713
  }
752
- if (isIdentifierReference(node, undefined, includeDefaultImports)) {
714
+ if (isIdentifierReference(node, isJavaScriptFile(sourceFile) ? undefined : names)) {
753
715
  nodes.push(node);
754
716
  return;
755
717
  }
@@ -758,10 +720,9 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
758
720
  const symbols = project.checker.getSymbolAtLocation(nodes);
759
721
  nodes.forEach((node, index) => {
760
722
  const rawSymbol = symbols[index];
761
- const symbol = resolveAlias(project.checker, rawSymbol);
762
- referenceNamespaces(project, node, symbol).forEach((namespace) => {
723
+ referenceNamespaces(project, node, rawSymbol).forEach((namespace) => {
763
724
  const entries = new Set([
764
- ...matchingAliasEntries(project, rawSymbol, namespace, state.targetsByKey),
725
+ ...matchingAliasEntries(project, rawSymbol, namespace, state),
765
726
  ...surfaceEntriesForNode(state, project, node, namespace),
766
727
  ]);
767
728
  entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
@@ -769,7 +730,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
769
730
  });
770
731
  };
771
732
 
772
- const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) => {
733
+ const scanSymbolUseProjects = (root, state, projects, names) => {
773
734
  const scannedFiles = new Set();
774
735
  let sourceScanCount = 0;
775
736
  for (const project of projects) {
@@ -778,7 +739,7 @@ const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) =>
778
739
  if (scannedFiles.has(fileIdentity)) continue;
779
740
  scannedFiles.add(fileIdentity);
780
741
  sourceScanCount += 1;
781
- scanSymbolUseFile(root, state, project, sourceFile, includeDefaultImports);
742
+ scanSymbolUseFile(root, state, project, sourceFile, names);
782
743
  }
783
744
  }
784
745
  return sourceScanCount;
@@ -787,10 +748,8 @@ const scanSymbolUseProjects = (root, state, projects, includeDefaultImports) =>
787
748
  const batchSymbolUseEvidence = (root, resolvedQueries, scanProjects, evidenceLimit) => {
788
749
  const state = symbolUseBatchState(resolvedQueries, evidenceLimit);
789
750
  resolvedQueries.forEach((entry) => registerSymbolTargets(root, state, entry));
790
- const includeDefaultImports = resolvedQueries.some(
791
- ({ query }) => query.symbol.exportedName === "default",
792
- );
793
- const sourceScanCount = scanSymbolUseProjects(root, state, scanProjects, includeDefaultImports);
751
+ const names = referenceNames(scanProjects, [...state.candidateNames, ...state.targetNames]);
752
+ const sourceScanCount = scanSymbolUseProjects(root, state, scanProjects, names);
794
753
  return {
795
754
  evidenceByQuery: state.evidenceByQuery,
796
755
  totalByQuery: state.totalByQuery,
@@ -0,0 +1,68 @@
1
+ import { SymbolFlags } from "typescript/unstable/sync";
2
+
3
+ import {
4
+ declarationName,
5
+ declarationNamespaces,
6
+ declarationsForSymbol,
7
+ stableDeclarationKey,
8
+ } from "./semantic-identity.mjs";
9
+
10
+ export const targetName = (declaration) => declarationName(declaration) ?? "default";
11
+
12
+ // A target key contains the declaration name. The name check skips the key
13
+ // and its file identity for every declaration that cannot match.
14
+ const targetsForDeclaration = (state, declaration, namespace) =>
15
+ state.targetNames.has(targetName(declaration))
16
+ ? (state.targetsByKey.get(stableDeclarationKey(declaration, namespace)) ?? [])
17
+ : [];
18
+
19
+ export const matchingSymbolEntries = (project, symbol, namespace, state) => {
20
+ const declarations = declarationsForSymbol(project, symbol);
21
+ const matchingLane = declarations.filter((declaration) =>
22
+ declarationNamespaces(declaration).has(namespace),
23
+ );
24
+ if (matchingLane.length > 0) {
25
+ return new Set(
26
+ matchingLane.flatMap((declaration) => targetsForDeclaration(state, declaration, namespace)),
27
+ );
28
+ }
29
+ return new Set(
30
+ declarations.flatMap((declaration) =>
31
+ [...declarationNamespaces(declaration)].flatMap((declaredNamespace) =>
32
+ targetsForDeclaration(state, declaration, declaredNamespace),
33
+ ),
34
+ ),
35
+ );
36
+ };
37
+
38
+ const resolveAliasEntries = (project, symbol, namespace, state) => {
39
+ const entries = new Set();
40
+ const seen = new Set();
41
+ let current = symbol;
42
+ while (current && !seen.has(current)) {
43
+ seen.add(current);
44
+ matchingSymbolEntries(project, current, namespace, state).forEach((entry) =>
45
+ entries.add(entry),
46
+ );
47
+ if ((current.flags & SymbolFlags.Alias) === 0) break;
48
+ const aliased = project.checker.getAliasedSymbol(current);
49
+ if (project.checker.isUnknownSymbol(aliased) || aliased === current) break;
50
+ current = aliased;
51
+ }
52
+ return entries;
53
+ };
54
+
55
+ // The scan visits every identifier, so one symbol occurs many times. Each
56
+ // checker call is a round trip, and the targets do not change during a batch,
57
+ // so the batch keeps one answer for each symbol and namespace.
58
+ export const matchingAliasEntries = (project, symbol, namespace, state) => {
59
+ if (!symbol) return new Set();
60
+ const bySymbol = state.aliasEntriesByProject.get(project) ?? new Map();
61
+ state.aliasEntriesByProject.set(project, bySymbol);
62
+ const byNamespace = bySymbol.get(symbol) ?? new Map();
63
+ bySymbol.set(symbol, byNamespace);
64
+ if (!byNamespace.has(namespace)) {
65
+ byNamespace.set(namespace, resolveAliasEntries(project, symbol, namespace, state));
66
+ }
67
+ return byNamespace.get(namespace);
68
+ };