ts-axi 0.1.17 → 0.1.18

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.
@@ -547,9 +547,91 @@ export function createSemanticProject(index, scope) {
547
547
  }
548
548
  return undefined;
549
549
  };
550
- const split = (absFile, pos) => {
550
+ /** Where findReferences stops on CommonJS: a `module.exports` property of the Symbol, and the locals of `const { x } = require()`. */
551
+ const commonJsHop = (fileName, start) => {
552
+ const sf = program.getSourceFile(fileName);
553
+ if (!sf)
554
+ return undefined;
555
+ const id = nodeAt(sf, start);
556
+ if (!ts.isIdentifier(id))
557
+ return undefined;
558
+ const parent = id.parent;
559
+ if ((ts.isShorthandPropertyAssignment(parent) && parent.name === id) || (ts.isPropertyAssignment(parent) && parent.initializer === id)) {
560
+ return isModuleExportsObject(parent.parent) ? parent.name.getStart(sf) : undefined;
561
+ }
562
+ const element = ts.isBindingElement(parent) ? parent : undefined;
563
+ if (!element || !ts.isIdentifier(element.name) || !ts.isObjectBindingPattern(element.parent))
564
+ return undefined;
565
+ const decl = element.parent.parent;
566
+ if (!ts.isVariableDeclaration(decl) || !decl.initializer || !isRequireCall(decl.initializer))
567
+ return undefined;
568
+ return element.name.getStart(sf);
569
+ };
570
+ /** findReferences of the Symbol at `pos`, with the CommonJS hops of `commonJsHop` merged into the target group. */
571
+ const referenceGroups = (absFile, pos) => {
551
572
  const twin = declarationTwin(absFile, pos);
552
573
  const groups = [...(service.findReferences(absFile, pos) ?? []), ...(twin ? service.findReferences(twin.file, twin.pos) ?? [] : [])];
574
+ const covers = (r) => resolve(r.fileName) === resolve(absFile) && r.textSpan.start <= pos && pos <= r.textSpan.start + r.textSpan.length;
575
+ const target = groups.find((g) => g.references.some(covers));
576
+ if (!target)
577
+ return { groups, extra: [] };
578
+ const seen = new Set(groups.flatMap((g) => g.references.map((r) => `${resolve(r.fileName)}#${r.textSpan.start}`)));
579
+ const extra = [];
580
+ const hops = new Set();
581
+ const queue = [...target.references];
582
+ for (let i = 0; i < queue.length && i < 500; i++) {
583
+ const hop = commonJsHop(queue[i].fileName, queue[i].textSpan.start);
584
+ const hopKey = `${resolve(queue[i].fileName)}#${hop}`;
585
+ if (hop === undefined || hops.has(hopKey))
586
+ continue;
587
+ hops.add(hopKey);
588
+ for (const g of service.findReferences(queue[i].fileName, hop) ?? []) {
589
+ for (const r of g.references) {
590
+ const key = `${resolve(r.fileName)}#${r.textSpan.start}`;
591
+ // A renamed local (`{ x: y }`) declares `y`; its uses are the References.
592
+ if (seen.has(key) || (r.isDefinition && resolve(r.fileName) === resolve(queue[i].fileName) && r.textSpan.start === hop && hop !== queue[i].textSpan.start))
593
+ continue;
594
+ seen.add(key);
595
+ const entry = { ...r, isDefinition: false };
596
+ extra.push(entry);
597
+ queue.push(entry);
598
+ }
599
+ }
600
+ }
601
+ return { groups: extra.length === 0 ? groups : groups.map((g) => (g === target ? { ...g, references: [...g.references, ...extra] } : g)), extra };
602
+ };
603
+ /** Call sites among CommonJS-hop References; call hierarchy does not reach a destructured `require()` local. */
604
+ const commonJsCalls = (refs) => {
605
+ const sites = [];
606
+ for (const ref of refs) {
607
+ const sf = program.getSourceFile(ref.fileName);
608
+ const loc = location(ref.fileName, ref.textSpan.start);
609
+ if (!sf || !loc)
610
+ continue;
611
+ const id = nodeAt(sf, ref.textSpan.start);
612
+ const call = id.parent;
613
+ if (!(ts.isCallExpression(call) || ts.isNewExpression(call)) || call.expression !== id)
614
+ continue;
615
+ const item = enclosingItem(sf, call);
616
+ sites.push({ ...loc, caller: callerName(item), item });
617
+ }
618
+ return sites;
619
+ };
620
+ /** Call hierarchy item of the nearest named function around `node`, else the file itself. */
621
+ const enclosingItem = (sf, node) => {
622
+ for (let n = node.parent; n && !ts.isSourceFile(n); n = n.parent) {
623
+ if (!ts.isFunctionLike(n))
624
+ continue;
625
+ const prepared = service.prepareCallHierarchy(sf.fileName, n.getStart(sf));
626
+ const item = Array.isArray(prepared) ? prepared[0] : prepared;
627
+ if (item && item.kind !== ts.ScriptElementKind.scriptElement)
628
+ return item;
629
+ }
630
+ const span = { start: 0, length: sf.end };
631
+ return { name: sf.fileName, kind: ts.ScriptElementKind.scriptElement, file: sf.fileName, span, selectionSpan: span };
632
+ };
633
+ const split = (absFile, pos) => {
634
+ const { groups } = referenceGroups(absFile, pos);
553
635
  const descriptors = [];
554
636
  let target = -1;
555
637
  let flagged = -1;
@@ -661,6 +743,7 @@ export function createSemanticProject(index, scope) {
661
743
  const seen = new Set();
662
744
  return items
663
745
  .flatMap((item) => splitSites(item.file, item.selectionSpan.start, [...service.provideCallHierarchyIncomingCalls(item.file, item.selectionSpan.start), ...twinCalls]))
746
+ .concat(commonJsCalls(referenceGroups(absFile, pos).extra))
664
747
  .filter((site) => {
665
748
  const key = `${site.file}:${site.line}:${site.col}`;
666
749
  return !seen.has(key) && Boolean(seen.add(key));
@@ -726,3 +809,20 @@ export function openProject(cwd, options = {}) {
726
809
  const index = createSyntacticIndex(root, project);
727
810
  return { cwd, root, location, index, ...(project.implicit ? { warning: IMPLICIT_WARNING } : {}) };
728
811
  }
812
+ /** The object of `module.exports = {...}`. */
813
+ function isModuleExportsObject(node) {
814
+ if (!ts.isObjectLiteralExpression(node))
815
+ return false;
816
+ let assign = node.parent;
817
+ while (ts.isParenthesizedExpression(assign))
818
+ assign = assign.parent;
819
+ if (!ts.isBinaryExpression(assign) || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
820
+ return false;
821
+ const left = assign.left;
822
+ return ts.isPropertyAccessExpression(left) && ts.isIdentifier(left.expression) && left.expression.text === "module" && left.name.text === "exports";
823
+ }
824
+ function isRequireCall(expr) {
825
+ while (ts.isParenthesizedExpression(expr))
826
+ expr = expr.expression;
827
+ return ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "require" && expr.arguments.length === 1;
828
+ }
@@ -441,6 +441,10 @@ export function collectDeclarations(sf, file) {
441
441
  nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported, signature: headUntil(prop, fn, sf), namePos: prop.name.getStart(sf) });
442
442
  else if (scope && ts.isIdentifier(value))
443
443
  aliasedMember(scope, value, name, holder, exported, prop.name);
444
+ else if (holder === "exports" && !ts.isIdentifier(value)) {
445
+ // `module.exports = { x: make(...) }`: the property is the only Declaration a destructured require can reach.
446
+ nested({ start: prop.getStart(sf), end: prop.end, kind: "property", name, parentName: holder, exported, signature: clip(tokenText(prop, sf, prop.getStart(sf), prop.end)), namePos: prop.name.getStart(sf) });
447
+ }
444
448
  }
445
449
  else if (scope && ts.isShorthandPropertyAssignment(prop)) {
446
450
  aliasedMember(scope, prop.name, name, holder, exported, prop.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-axi",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "AXI-compliant TypeScript symbol CLI for agents - definitions, references, callers, file outlines and change impact (blast radius) with token-efficient TOON output",
5
5
  "type": "module",
6
6
  "keywords": [