ts-axi 0.1.16 → 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
+ }
@@ -390,6 +390,25 @@ export function collectDeclarations(sf, file) {
390
390
  !(ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node)),
391
391
  });
392
392
  };
393
+ const classMembers = (cls, exported, qualified) => {
394
+ for (const member of cls.members) {
395
+ if (ts.isSemicolonClassElement(member) || ts.isClassStaticBlockDeclaration(member) || ts.isIndexSignatureDeclaration(member))
396
+ continue;
397
+ const memberName = ts.isConstructorDeclaration(member) ? "constructor" : nameText(member.name, sf);
398
+ if (!memberName)
399
+ continue;
400
+ const nameNode = ts.isConstructorDeclaration(member)
401
+ ? member.getChildren(sf).find((c) => c.kind === ts.SyntaxKind.ConstructorKeyword)
402
+ : member.name;
403
+ add(member, nameNode, memberName, exported && !isPrivateMember(member), qualified);
404
+ }
405
+ };
406
+ /** `module.exports = class X {...}`, `exports.X = class {...}`, `export default class {...}` expression. */
407
+ const exportedClass = (cls, fallback) => {
408
+ const name = cls.name?.text ?? fallback;
409
+ add(cls, cls.name, name, true);
410
+ classMembers(cls, true, name);
411
+ };
393
412
  const nested = (raw) => {
394
413
  raws.push({ ...raw, qualifiedName: raw.qualifiedName ?? (raw.parentName ? `${raw.parentName}.${raw.name}` : raw.name), hasBody: true });
395
414
  };
@@ -422,6 +441,10 @@ export function collectDeclarations(sf, file) {
422
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) });
423
442
  else if (scope && ts.isIdentifier(value))
424
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
+ }
425
448
  }
426
449
  else if (scope && ts.isShorthandPropertyAssignment(prop)) {
427
450
  aliasedMember(scope, prop.name, name, holder, exported, prop.name);
@@ -737,7 +760,9 @@ export function collectDeclarations(sf, file) {
737
760
  return;
738
761
  const target = compact(left, sf);
739
762
  if (target === "module.exports") {
740
- if (ts.isObjectLiteralExpression(right))
763
+ if (ts.isClassExpression(right))
764
+ exportedClass(right, "exports");
765
+ else if (ts.isObjectLiteralExpression(right))
741
766
  objectMembers(right, "exports", true);
742
767
  else if (asFunction(right))
743
768
  returnedMembers(asFunction(right).body, "exports", true);
@@ -746,6 +771,8 @@ export function collectDeclarations(sf, file) {
746
771
  return;
747
772
  }
748
773
  if (target === `exports.${left.name.text}` || target === `module.exports.${left.name.text}`) {
774
+ if (ts.isClassExpression(right))
775
+ return exportedClass(right, left.name.text);
749
776
  if (ts.isObjectLiteralExpression(right))
750
777
  objectMembers(right, left.name.text, true);
751
778
  else
@@ -829,7 +856,9 @@ export function collectDeclarations(sf, file) {
829
856
  }
830
857
  else if (ts.isExportAssignment(statement)) {
831
858
  const value = unwrap(statement.expression);
832
- if (ts.isObjectLiteralExpression(value))
859
+ if (ts.isClassExpression(value))
860
+ exportedClass(value, "default");
861
+ else if (ts.isObjectLiteralExpression(value))
833
862
  objectMembers(value, "default", true);
834
863
  else
835
864
  stateArray(statement.expression);
@@ -876,17 +905,7 @@ export function collectDeclarations(sf, file) {
876
905
  add(statement, statement.name, name, exported, parent);
877
906
  const qualified = parent ? `${parent}.${name}` : name;
878
907
  if (ts.isClassDeclaration(statement)) {
879
- for (const member of statement.members) {
880
- if (ts.isSemicolonClassElement(member) || ts.isClassStaticBlockDeclaration(member) || ts.isIndexSignatureDeclaration(member))
881
- continue;
882
- const memberName = ts.isConstructorDeclaration(member) ? "constructor" : nameText(member.name, sf);
883
- if (!memberName)
884
- continue;
885
- const nameNode = ts.isConstructorDeclaration(member)
886
- ? member.getChildren(sf).find((c) => c.kind === ts.SyntaxKind.ConstructorKeyword)
887
- : member.name;
888
- add(member, nameNode, memberName, exported && !isPrivateMember(member), qualified);
889
- }
908
+ classMembers(statement, exported, qualified);
890
909
  }
891
910
  else if (ts.isInterfaceDeclaration(statement)) {
892
911
  for (const member of statement.members) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-axi",
3
- "version": "0.1.16",
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": [