ts-axi 0.1.11 → 0.1.13

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.
@@ -278,6 +278,38 @@ function registeredFunction(arg) {
278
278
  return asFunction(arg.elements[arg.elements.length - 1]);
279
279
  return asFunction(arg);
280
280
  }
281
+ /**
282
+ * A registration's callback given by identifier (`.directive('x', fancyBox)`, `['$a', fancyBox]`): the
283
+ * function declaration or function-valued variable of that name among the statements of the call's scope.
284
+ */
285
+ function functionByIdentifier(call, arg) {
286
+ const ref = arg && ts.isArrayLiteralExpression(arg) ? arg.elements[arg.elements.length - 1] : arg;
287
+ if (!ref || !ts.isIdentifier(ref))
288
+ return undefined;
289
+ let scope = call;
290
+ while (!ts.isSourceFile(scope) && !(ts.isBlock(scope) && ts.isFunctionLike(scope.parent)))
291
+ scope = scope.parent;
292
+ const statements = scope.statements;
293
+ let found;
294
+ let inject;
295
+ for (const st of statements) {
296
+ if (ts.isFunctionDeclaration(st) && st.name?.text === ref.text && st.body)
297
+ found = { fn: st, name: st.name };
298
+ else if (ts.isVariableStatement(st)) {
299
+ for (const d of st.declarationList.declarations) {
300
+ const fn = asFunction(d.initializer);
301
+ if (fn && ts.isIdentifier(d.name) && d.name.text === ref.text)
302
+ found = { fn, name: d.name };
303
+ }
304
+ }
305
+ else if (ts.isExpressionStatement(st) && ts.isBinaryExpression(st.expression) && ts.isPropertyAccessExpression(st.expression.left)) {
306
+ const { left, right } = st.expression;
307
+ if (left.name.text === "$inject" && ts.isIdentifier(left.expression) && left.expression.text === ref.text && ts.isArrayLiteralExpression(right))
308
+ inject = right;
309
+ }
310
+ }
311
+ return found && (inject ? { ...found, inject } : found);
312
+ }
281
313
  function isStringName(node) {
282
314
  return node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node));
283
315
  }
@@ -303,6 +335,8 @@ function headUntil(node, fn, sf) {
303
335
  export function collectDeclarations(sf, file) {
304
336
  const raws = [];
305
337
  const local = localExportNames(sf);
338
+ /** Body spans of functions registered by identifier. */
339
+ const registeredBodies = [];
306
340
  const add = (node, nameNode, name, exported, parent) => {
307
341
  const kind = kindOf(node);
308
342
  if (!kind)
@@ -353,23 +387,22 @@ export function collectDeclarations(sf, file) {
353
387
  if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
354
388
  continue;
355
389
  const { left, operatorToken, right } = statement.expression;
390
+ if (operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isPropertyAccessExpression(left) || left.expression.kind !== ts.SyntaxKind.ThisKeyword)
391
+ continue;
392
+ const value = unwrap(right);
393
+ if (ts.isObjectLiteralExpression(value))
394
+ objectMembers(value, `${owner}.${left.name.text}`, exported);
356
395
  const fn = asFunction(right);
357
- if (operatorToken.kind !== ts.SyntaxKind.EqualsToken || !fn || !ts.isPropertyAccessExpression(left) || left.expression.kind !== ts.SyntaxKind.ThisKeyword)
396
+ if (!fn)
358
397
  continue;
359
398
  nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: owner, exported, signature: headUntil(statement.expression, fn, sf), namePos: left.name.getStart(sf) });
360
399
  }
361
400
  };
362
- /** Top-level object literals returned by a factory: its API. */
363
- const returnedMembers = (body, owner) => {
364
- if (ts.isObjectLiteralExpression(unwrap(body)))
365
- return objectMembers(unwrap(body), owner, true);
366
- if (!ts.isBlock(body))
367
- return;
368
- for (const statement of body.statements) {
369
- if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(unwrap(statement.expression))) {
370
- objectMembers(unwrap(statement.expression), owner, true);
371
- }
372
- }
401
+ /** Object literals a function returns at its top level (a factory's API, a deps object): their function-valued members. */
402
+ const returnedMembers = (body, owner, exported) => {
403
+ if (body)
404
+ for (const obj of returnedObjects(body))
405
+ objectMembers(obj, owner, exported);
373
406
  };
374
407
  const topLevelExported = (name) => raws.find((r) => r.qualifiedName === name && r.parentName === undefined)?.exported ?? false;
375
408
  const isConstructorName = (name) => /^[A-Z]/.test(name);
@@ -389,10 +422,14 @@ export function collectDeclarations(sf, file) {
389
422
  bindingMembers(arg, name);
390
423
  return;
391
424
  }
392
- const fn = registeredFunction(arg);
393
- if (!fn)
425
+ const inline = registeredFunction(arg);
426
+ // By identifier: namePos is the function's own name, which links the two Declarations (`registrationOf`).
427
+ const byName = inline ? undefined : functionByIdentifier(call, arg);
428
+ const fn = inline ?? byName?.fn;
429
+ if (!fn || !fn.body)
394
430
  return;
395
- const inject = arg && ts.isArrayLiteralExpression(arg) ? ` [${arg.elements.slice(0, -1).map((e) => e.getText(sf)).join(", ")}]` : "";
431
+ const injectList = arg && ts.isArrayLiteralExpression(arg) ? arg.elements.slice(0, -1) : (byName?.inject?.elements ?? []);
432
+ const inject = injectList.length > 0 || (arg && ts.isArrayLiteralExpression(arg)) ? ` [${injectList.map((e) => e.getText(sf)).join(", ")}]` : "";
396
433
  nested({
397
434
  start,
398
435
  end: arg.end,
@@ -402,21 +439,27 @@ export function collectDeclarations(sf, file) {
402
439
  parentName: verb,
403
440
  exported: true,
404
441
  signature: `${verb} ${name}${paramsText(fn, sf)}`,
405
- namePos: (ts.isFunctionExpression(fn) && fn.name ? fn.name : fn).getStart(sf),
442
+ namePos: (byName ? byName.name : ts.isFunctionExpression(fn) && fn.name ? fn.name : fn).getStart(sf),
406
443
  nameless: true,
407
444
  });
408
445
  // The DI array is compared, not displayed: reordering injections is a contract change.
409
446
  if (inject)
410
447
  raws[raws.length - 1].signature += `\n${inject.trim()}`;
448
+ const from = raws.length;
411
449
  if (CONSTRUCTOR_VERBS.has(verb))
412
450
  thisMembers(fn.body, name, true);
413
451
  if (verb === "factory" || verb === "service")
414
- returnedMembers(fn.body, name);
452
+ returnedMembers(fn.body, name, true);
415
453
  if (verb === "directive")
416
454
  for (const obj of returnedObjects(fn.body))
417
455
  bindingMembers(obj, name);
418
456
  if (verb === "config" || verb === "run")
419
457
  routerBody(fn.body);
458
+ if (byName) {
459
+ for (const r of raws.slice(from))
460
+ r.registered = true;
461
+ registeredBodies.push([fn.body.getStart(sf), fn.body.end]);
462
+ }
420
463
  };
421
464
  /** Object literals a function returns at its top level (a directive definition object). */
422
465
  const returnedObjects = (body) => {
@@ -628,18 +671,26 @@ export function collectDeclarations(sf, file) {
628
671
  if (target === "module.exports") {
629
672
  if (ts.isObjectLiteralExpression(right))
630
673
  objectMembers(right, "exports", true);
674
+ else if (asFunction(right))
675
+ returnedMembers(asFunction(right).body, "exports", true);
631
676
  else
632
677
  stateArray(right);
633
678
  return;
634
679
  }
635
- if (target === `exports.${left.name.text}` || target === `module.exports.${left.name.text}`)
636
- stateArray(right);
680
+ if (target === `exports.${left.name.text}` || target === `module.exports.${left.name.text}`) {
681
+ if (ts.isObjectLiteralExpression(right))
682
+ objectMembers(right, left.name.text, true);
683
+ else
684
+ stateArray(right);
685
+ }
637
686
  const owner = left.expression;
638
687
  if (ts.isPropertyAccessExpression(owner) && owner.name.text === "prototype" && ts.isIdentifier(owner.expression)) {
639
688
  const cls = owner.expression.text;
640
689
  const fn = asFunction(right);
641
- if (fn)
690
+ if (fn) {
642
691
  nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: cls, exported: topLevelExported(cls), signature: headUntil(expr, fn, sf), namePos: left.name.getStart(sf) });
692
+ returnedMembers(fn.body, left.name.text, topLevelExported(cls));
693
+ }
643
694
  return;
644
695
  }
645
696
  if (ts.isIdentifier(left.name) && left.name.text === "prototype" && ts.isIdentifier(owner) && ts.isObjectLiteralExpression(right)) {
@@ -654,6 +705,7 @@ export function collectDeclarations(sf, file) {
654
705
  return;
655
706
  const exported = holder === "exports" || topLevelExported(holder);
656
707
  nested({ start: statement.getStart(sf), end: statement.end, kind: "method", name: left.name.text, parentName: holder, exported, signature: headUntil(expr, fn, sf), namePos: left.name.getStart(sf) });
708
+ returnedMembers(fn.body, left.name.text, exported);
657
709
  };
658
710
  /** Script-style patterns of one module-level statement; `inIife` adds the IIFE's own functions. */
659
711
  const scriptStatement = (statement, inIife) => {
@@ -662,8 +714,10 @@ export function collectDeclarations(sf, file) {
662
714
  return;
663
715
  if (inIife)
664
716
  add(statement, statement.name, statement.name.text, false);
717
+ const exported = inIife ? false : topLevelExported(statement.name.text);
665
718
  if (isConstructorName(statement.name.text))
666
- thisMembers(statement.body, statement.name.text, inIife ? false : topLevelExported(statement.name.text));
719
+ thisMembers(statement.body, statement.name.text, exported);
720
+ returnedMembers(statement.body, statement.name.text, exported);
667
721
  if (takesRouter(statement))
668
722
  routerBody(statement.body);
669
723
  }
@@ -680,6 +734,8 @@ export function collectDeclarations(sf, file) {
680
734
  const exported = inIife ? false : topLevelExported(name);
681
735
  const init = unwrap(decl.initializer);
682
736
  stateArray(init);
737
+ if (fn)
738
+ returnedMembers(fn.body, name, exported);
683
739
  if (ts.isObjectLiteralExpression(init))
684
740
  objectMembers(init, name, exported);
685
741
  else if (fn && isConstructorName(name))
@@ -704,7 +760,11 @@ export function collectDeclarations(sf, file) {
704
760
  callChain(expr);
705
761
  }
706
762
  else if (ts.isExportAssignment(statement)) {
707
- stateArray(statement.expression);
763
+ const value = unwrap(statement.expression);
764
+ if (ts.isObjectLiteralExpression(value))
765
+ objectMembers(value, "default", true);
766
+ else
767
+ stateArray(statement.expression);
708
768
  }
709
769
  else if (ts.isIfStatement(statement)) {
710
770
  scriptStatement(statement.thenStatement, inIife);
@@ -787,8 +847,11 @@ export function collectDeclarations(sf, file) {
787
847
  }
788
848
  };
789
849
  visitStatements(sf.statements, undefined, true);
790
- // Nested declarations are found after their holder statement; source order is start order.
791
- const ordered = raws.map((r, i) => ({ r, i })).sort((a, b) => a.r.start - b.r.start || a.i - b.i);
850
+ // A registered function's members belong to its registered name; the generic pass (returned objects, `this.x`) is dropped.
851
+ const kept = registeredBodies.length === 0
852
+ ? raws
853
+ : raws.filter((r) => r.registered || !registeredBodies.some(([s, e]) => r.namePos >= s && r.namePos < e));
854
+ const ordered = kept.map((r, i) => ({ r, i })).sort((a, b) => a.r.start - b.r.start || a.i - b.i);
792
855
  return ordered.map(({ r }) => {
793
856
  const startLc = sf.getLineAndCharacterOfPosition(r.start);
794
857
  const full = r.signature;
@@ -812,6 +875,12 @@ export function collectDeclarations(sf, file) {
812
875
  };
813
876
  });
814
877
  }
878
+ /** The string registration of a function registered by its identifier (`.directive('fancyBox', fancyBox)`): same namePos. */
879
+ export function registrationOf(decl, fileDecls) {
880
+ if (decl.nameless)
881
+ return undefined;
882
+ return fileDecls.find((d) => d.nameless === true && d.namePos === decl.namePos && d.file === decl.file && d.kind === "function");
883
+ }
815
884
  /** Declarations sharing file and qualified name (overloads, merging) are one Symbol. */
816
885
  export function symbolKey(decl) {
817
886
  return `${decl.file}#${decl.qualifiedName}`;
@@ -2,6 +2,7 @@
2
2
  // elements/attributes, their bindings as attributes, controllers in `ng-controller`, filters
3
3
  // after `|`. One regex pass per template line, no HTML parsing.
4
4
  import { sourceFs } from "./fs.js";
5
+ import { registrationOf } from "./symbols.js";
5
6
  import { listFiles, toRel } from "./walk.js";
6
7
  export const TEMPLATE_EXTENSIONS = [".html", ".htm", ".ejs", ".hbs", ".njk", ".pug"];
7
8
  export function isTemplatePath(path) {
@@ -20,6 +21,12 @@ function elementOrAttribute(name) {
20
21
  }
21
22
  /** A Declaration's template form; undefined when templates cannot name it. */
22
23
  export function templateTarget(decl, fileDecls) {
24
+ const registered = registrationOf(decl, fileDecls);
25
+ // A function registered by identifier uses the registered name; the row keeps the function's name.
26
+ if (registered) {
27
+ const target = templateTarget(registered, fileDecls);
28
+ return target && { ...target, symbol: decl.qualifiedName };
29
+ }
23
30
  const isComponent = (d) => d !== undefined && d.nameless === true && d.signature.startsWith("component ");
24
31
  if (decl.nameless && (decl.parentName === "directive" || isComponent(decl)))
25
32
  return { symbol: decl.qualifiedName, pattern: elementOrAttribute(decl.name) };
@@ -29,7 +36,8 @@ export function templateTarget(decl, fileDecls) {
29
36
  if (decl.nameless && decl.parentName === "filter")
30
37
  return { symbol: decl.qualifiedName, pattern: new RegExp(`(?<!\\|)\\|\\s*${escape(decl.name)}(?![\\w$])`, "g") };
31
38
  if (decl.kind === "property" && decl.parentName !== undefined) {
32
- const owner = fileDecls.find((d) => d.qualifiedName === decl.parentName && d.parentName !== decl.parentName);
39
+ const owners = fileDecls.filter((d) => d.qualifiedName === decl.parentName && d.parentName !== decl.parentName);
40
+ const owner = owners.find((d) => d.nameless) ?? owners[0];
33
41
  if (owner && owner.nameless && (owner.parentName === "directive" || isComponent(owner))) {
34
42
  return {
35
43
  symbol: decl.qualifiedName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-axi",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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": [
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ts-axi
3
- description: "Navigate TypeScript and JavaScript code and measure change impact through the ts-axi CLI - find where a symbol is declared, list its references and callers, outline a file, and see which other code a diff affects. Use before reading many files to find callers or usages of a function, class or type; use `ts-axi impact` when reviewing a diff, a pull request or your own uncommitted changes."
3
+ description: "Navigate TypeScript and JavaScript code and measure change impact through the ts-axi CLI - find where a symbol is declared, list its references and callers, outline a file, and see which other code a diff affects. Use before reading many files to find callers or usages of a function, class or type; use `ts-axi impact` when reviewing a diff, a pull request or your own uncommitted changes, and `ts-axi anchor` to check that review-comment file:line anchors are in the diff before posting them."
4
4
  user-invocable: false
5
5
  ---
6
6