fallow-type-aware 3.17.0 → 3.18.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.17.0",
3
+ "version": "3.18.0",
4
4
  "description": "Optional TypeScript-Go semantic refinement sidecar for Fallow",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -237,9 +237,15 @@ export const symbolForDeclaration = (project, declaration) =>
237
237
 
238
238
  export const resolveAlias = (checker, symbol) => {
239
239
  if (!symbol) return undefined;
240
- if ((symbol.flags & SymbolFlags.Alias) === 0) return symbol;
241
- const aliased = checker.getAliasedSymbol(symbol);
242
- return checker.isUnknownSymbol(aliased) ? symbol : aliased;
240
+ const seen = new Set();
241
+ let current = symbol;
242
+ while ((current.flags & SymbolFlags.Alias) !== 0 && !seen.has(current)) {
243
+ seen.add(current);
244
+ const aliased = checker.getAliasedSymbol(current);
245
+ if (checker.isUnknownSymbol(aliased) || aliased === current) return current;
246
+ current = aliased;
247
+ }
248
+ return current;
243
249
  };
244
250
 
245
251
  export const declarationsForSymbol = (project, symbol) =>
package/src/semantic.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isImportTypeNode,
21
21
  isNamespaceImport,
22
22
  isPropertyAccessExpression,
23
+ isQualifiedName,
23
24
  isPrivateIdentifier,
24
25
  isSetAccessorDeclaration,
25
26
  isStringLiteralLikeNode,
@@ -431,30 +432,60 @@ const registerSymbolTargets = (root, state, entry) => {
431
432
  state.exportEntriesByProject.set(project, projectEntries);
432
433
  });
433
434
  }
434
- entry.resolved.ownerContexts.forEach(({ project, declaration }) => {
435
- const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
436
- declarationsForSymbol(project, symbol).forEach((target) => {
437
- const namespaces =
438
- entry.query.symbol.declarationKind === "export"
439
- ? declarationNamespaces(target)
440
- : new Set([entry.query.symbol.namespace]);
441
- namespaces.forEach((namespace) => addSymbolTarget(root, state, entry, target, namespace));
442
- });
435
+ entry.resolved.ownerContexts.forEach(({ declaration, anchor }) => {
436
+ const namespaces =
437
+ entry.query.symbol.declarationKind === "export"
438
+ ? declarationNamespaces(declaration)
439
+ : new Set([entry.query.symbol.namespace]);
440
+ namespaces.forEach((namespace) => addSymbolTarget(root, state, entry, declaration, namespace));
441
+ if (entry.query.symbol.declarationKind === "export" && anchor) {
442
+ declarationNamespaces(anchor).forEach((namespace) =>
443
+ addSymbolTarget(root, state, entry, anchor, namespace),
444
+ );
445
+ }
443
446
  });
444
447
  state.contractRelationsByQuery.set(entry.query.id, entry.resolved.contractRelations);
445
448
  };
446
449
 
447
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
+ }
448
462
  return new Set(
449
- declarationsForSymbol(project, symbol)
450
- .filter((declaration) => declarationNamespaces(declaration).has(namespace))
451
- .flatMap((declaration) => {
452
- const key = stableDeclarationKey(declaration, namespace);
453
- return targetsByKey.get(key) ?? [];
454
- }),
463
+ declarations.flatMap((declaration) =>
464
+ [...declarationNamespaces(declaration)].flatMap(
465
+ (declaredNamespace) =>
466
+ targetsByKey.get(stableDeclarationKey(declaration, declaredNamespace)) ?? [],
467
+ ),
468
+ ),
455
469
  );
456
470
  };
457
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
+
458
489
  const isDeclarationReference = (root, state, entry, node) => {
459
490
  const locations = state.declarationLocationsByQuery.get(entry.query.id);
460
491
  if (locations.has(locationKey(location(root, node)))) return true;
@@ -488,13 +519,37 @@ const importTypeSpecifier = (node) => {
488
519
  const isDynamicImportCall = (node) =>
489
520
  isCallExpression(node) && node.expression.getText(node.getSourceFile()) === "import";
490
521
 
522
+ const MODULE_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
523
+
524
+ const lexicalModuleIdentityForText = (project, sourceFile, specifierText) => {
525
+ if (!specifierText?.startsWith(".")) return undefined;
526
+ const resolved = path.resolve(path.dirname(sourceFile.fileName), specifierText);
527
+ const extension = path.extname(resolved);
528
+ const stem = extension ? resolved.slice(0, -extension.length) : resolved;
529
+ const candidates = [
530
+ resolved,
531
+ ...MODULE_SOURCE_EXTENSIONS.map((candidate) => `${stem}${candidate}`),
532
+ ...MODULE_SOURCE_EXTENSIONS.map((candidate) => path.join(resolved, `index${candidate}`)),
533
+ ];
534
+ for (const candidate of candidates) {
535
+ const matched = project.program.getSourceFile(candidate);
536
+ if (matched) return sourceFileIdentity(matched);
537
+ if (existsSync(candidate)) return canonicalFileIdentity(candidate);
538
+ }
539
+ return undefined;
540
+ };
541
+
542
+ const lexicalModuleIdentity = (project, specifier) =>
543
+ specifier
544
+ ? lexicalModuleIdentityForText(project, specifier.getSourceFile(), specifier.text)
545
+ : undefined;
546
+
491
547
  const moduleIdentityForSpecifier = (project, specifier) => {
492
548
  if (!specifier) return undefined;
549
+ const lexicalIdentity = lexicalModuleIdentity(project, specifier);
550
+ if (lexicalIdentity) return lexicalIdentity;
493
551
  const moduleSymbol = project.checker.getSymbolAtLocation(specifier);
494
- const moduleDeclaration = declarationsForSymbol(
495
- project,
496
- resolveAlias(project.checker, moduleSymbol),
497
- )[0];
552
+ const moduleDeclaration = declarationsForSymbol(project, moduleSymbol)[0];
498
553
  return moduleDeclaration ? sourceFileIdentity(moduleDeclaration.getSourceFile()) : undefined;
499
554
  };
500
555
 
@@ -507,7 +562,9 @@ const namespaceImportDeclaration = (project, node) => {
507
562
  : undefined
508
563
  : isElementAccessExpression(parent) && parent.argumentExpression === node
509
564
  ? parent.expression
510
- : undefined;
565
+ : isQualifiedName(parent) && parent.right === node
566
+ ? parent.left
567
+ : undefined;
511
568
  if (!namespace) return undefined;
512
569
  if (!isIdentifier(namespace)) return undefined;
513
570
  const symbol = project.checker.getSymbolAtLocation(namespace);
@@ -516,9 +573,28 @@ const namespaceImportDeclaration = (project, node) => {
516
573
  .find(isNamespaceImport);
517
574
  };
518
575
 
576
+ const localAliasModuleEdgeDeclaration = (project, node) => {
577
+ const symbol = project.checker.getSymbolAtLocation(node);
578
+ return symbol?.declarations
579
+ ?.map((declaration) => moduleEdgeDeclaration(declaration.resolve(project)))
580
+ .find(Boolean);
581
+ };
582
+
519
583
  const referencedModuleIdentity = (project, node) => {
584
+ const importType = ancestorImportType(node);
585
+ const lexicalImportType = importType
586
+ ?.getText(importType.getSourceFile())
587
+ .match(/import\s*\(\s*["']([^"']+)["']/u)?.[1];
588
+ const lexicalIdentity = lexicalModuleIdentityForText(
589
+ project,
590
+ node.getSourceFile(),
591
+ lexicalImportType,
592
+ );
593
+ if (lexicalIdentity) return lexicalIdentity;
520
594
  const declaration =
521
- moduleEdgeDeclaration(node) ?? moduleEdgeDeclaration(namespaceImportDeclaration(project, node));
595
+ moduleEdgeDeclaration(node) ??
596
+ moduleEdgeDeclaration(namespaceImportDeclaration(project, node)) ??
597
+ localAliasModuleEdgeDeclaration(project, node);
522
598
  const declarationSpecifier = declaration?.moduleSpecifier;
523
599
  const specifier =
524
600
  declarationSpecifier && isStringLiteralLikeNode(declarationSpecifier)
@@ -527,13 +603,22 @@ const referencedModuleIdentity = (project, node) => {
527
603
  return moduleIdentityForSpecifier(project, specifier);
528
604
  };
529
605
 
530
- const isExactExportReference = (project, entry, node) =>
531
- entry.query.symbol.declarationKind !== "export" ||
532
- referencedModuleIdentity(project, node) ===
533
- canonicalFileIdentity(entry.query.symbol.absolutePath);
606
+ const isExactExportReference = (project, entry, node) => {
607
+ if (entry.query.symbol.declarationKind !== "export") return true;
608
+ const referencedModule = referencedModuleIdentity(project, node);
609
+ if (!referencedModule) return false;
610
+ const queryModule = canonicalFileIdentity(entry.query.symbol.absolutePath);
611
+ return (
612
+ referencedModule === queryModule ||
613
+ sourceFileIdentity(entry.resolved.declaration.getSourceFile()) === queryModule
614
+ );
615
+ };
534
616
 
535
617
  const recordSymbolUse = (root, state, project, entry, node, namespace) => {
536
618
  if (isDeclarationReference(root, state, entry, node)) return;
619
+ if (isDeclaration(node.parent) && node.parent.name === node) return;
620
+ const moduleEdge = moduleEdgeDeclaration(node);
621
+ if (moduleEdge) return;
537
622
  if (!isExactExportReference(project, entry, node)) return;
538
623
  const evidence = state.evidenceByQuery.get(entry.query.id);
539
624
  state.totalByQuery.set(entry.query.id, state.totalByQuery.get(entry.query.id) + 1);
@@ -635,6 +720,26 @@ const referenceNamespaces = (project, node, symbol) => {
635
720
  return namespaces.size > 0 ? [...namespaces] : [isTypePosition(node) ? "type" : "value"];
636
721
  };
637
722
 
723
+ const surfaceEntriesForNode = (state, project, node, namespace) => {
724
+ if (moduleEdgeDeclaration(node)) return new Set();
725
+ const moduleIdentity = referencedModuleIdentity(project, node);
726
+ if (!moduleIdentity) return new Set();
727
+ const direct = [...(state.exportEntriesByModule.get(moduleIdentity) ?? [])].filter(
728
+ (entry) =>
729
+ entry.query.symbol.exportedName === node.text && entry.query.symbol.namespace === namespace,
730
+ );
731
+ const entries = new Set(direct);
732
+ for (const entry of direct) {
733
+ for (const { declaration } of entry.resolved.ownerContexts) {
734
+ const symbol = resolveAlias(project.checker, symbolForDeclaration(project, declaration));
735
+ matchingSymbolEntries(project, symbol, namespace, state.targetsByKey).forEach((related) =>
736
+ entries.add(related),
737
+ );
738
+ }
739
+ }
740
+ return entries;
741
+ };
742
+
638
743
  const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImports) => {
639
744
  const projectEntries = classMemberEntrySetForProject(state, project);
640
745
  const nodes = [];
@@ -644,7 +749,7 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
644
749
  recordStringDispatchedMemberAccess(state, projectEntries, node);
645
750
  recordComputedMemberAccess(state, project, projectEntries, node);
646
751
  }
647
- if (isIdentifierReference(node, state.candidateNames, includeDefaultImports)) {
752
+ if (isIdentifierReference(node, undefined, includeDefaultImports)) {
648
753
  nodes.push(node);
649
754
  return;
650
755
  }
@@ -652,9 +757,13 @@ const scanSymbolUseFile = (root, state, project, sourceFile, includeDefaultImpor
652
757
  });
653
758
  const symbols = project.checker.getSymbolAtLocation(nodes);
654
759
  nodes.forEach((node, index) => {
655
- const symbol = resolveAlias(project.checker, symbols[index]);
760
+ const rawSymbol = symbols[index];
761
+ const symbol = resolveAlias(project.checker, rawSymbol);
656
762
  referenceNamespaces(project, node, symbol).forEach((namespace) => {
657
- const entries = matchingSymbolEntries(project, symbol, namespace, state.targetsByKey);
763
+ const entries = new Set([
764
+ ...matchingAliasEntries(project, rawSymbol, namespace, state.targetsByKey),
765
+ ...surfaceEntriesForNode(state, project, node, namespace),
766
+ ]);
658
767
  entries.forEach((entry) => recordSymbolUse(root, state, project, entry, node, namespace));
659
768
  });
660
769
  });
@@ -956,7 +1065,7 @@ const resolvedOwnerContext = (query, owner) => {
956
1065
  const anchor = symbolQueryAnchor(owner.project, query);
957
1066
  const target = resolvedAnchorTarget(owner.project, anchor, query.symbol);
958
1067
  return symbolQueryResolved(owner.project, query.symbol, anchor, target)
959
- ? { ...owner, ...target }
1068
+ ? { ...owner, anchor, ...target }
960
1069
  : undefined;
961
1070
  };
962
1071