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.
- package/README.md +28 -3
- package/dist/src/anchor.js +113 -0
- package/dist/src/cli.js +7 -3
- package/dist/src/commands/anchor.js +250 -0
- package/dist/src/commands/common.js +47 -21
- package/dist/src/commands/def.js +4 -2
- package/dist/src/commands/impact.js +122 -25
- package/dist/src/commands/refs.js +27 -8
- package/dist/src/diff.js +20 -6
- package/dist/src/errors.js +2 -0
- package/dist/src/git.js +3 -3
- package/dist/src/skill.js +2 -1
- package/dist/src/symbols.js +93 -24
- package/dist/src/templates.js +9 -1
- package/package.json +1 -1
- package/skills/ts-axi/SKILL.md +1 -1
package/dist/src/symbols.js
CHANGED
|
@@ -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 (
|
|
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
|
-
/**
|
|
363
|
-
const returnedMembers = (body, owner) => {
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
393
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
791
|
-
const
|
|
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}`;
|
package/dist/src/templates.js
CHANGED
|
@@ -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
|
|
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.
|
|
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": [
|
package/skills/ts-axi/SKILL.md
CHANGED
|
@@ -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
|
|