ts-axi 0.1.13 → 0.1.15
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 +32 -1
- package/dist/src/commands/common.js +2 -1
- package/dist/src/commands/impact.js +10 -4
- package/dist/src/commands/refs.js +2 -2
- package/dist/src/references.js +4 -2
- package/dist/src/symbols.js +97 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,37 @@ Early, but already used in real reviews. What the numbers say so far:
|
|
|
50
50
|
|
|
51
51
|
Known limits: `.astro` / `.vue` / `.svelte` files are not analysed (they appear under `skipped`, and `--grep` searches them textually); every semantic command starts cold (0.3 s on a small project, 2-5 s on the monorepo, dominated by the library `.d.ts` files that barrels pull in); import specifiers are extracted textually, so computed specifiers and bundler-only aliases are missed.
|
|
52
52
|
|
|
53
|
+
## Token cost
|
|
54
|
+
|
|
55
|
+
`test/integration/token-compare.test.ts` does six agent tasks twice: with ts-axi (plus one followed hint where that is the normal next step), and the way a careful agent does them without it - at most 3 `git grep -n` calls, then a ±40-line numbered read around each relevant match (overlaps merged), with `git diff` first for impact. Tokens are characters / 4. A recall gate fails when a grep hit in code (not a comment, a string or a doc file) is neither listed by ts-axi nor covered by its `textual` / `templates` count.
|
|
56
|
+
|
|
57
|
+
<!-- token-compare:start -->
|
|
58
|
+
| task | repo | ts-axi tokens | grep tokens | ratio | recall |
|
|
59
|
+
| --- | --- | ---: | ---: | ---: | --- |
|
|
60
|
+
| def of a class method | astro sample | 117 | 266 | 0.44 | 100% (1/1) |
|
|
61
|
+
| callers of a function | astro sample | 227 | 1169 | 0.19 | 100% (8/8) |
|
|
62
|
+
| refs of a class member | astro sample | 216 | 1133 | 0.19 | 100% (8/8) |
|
|
63
|
+
| impact of a signature change | astro sample | 737 | 1304 | 0.56 | 100% (14/14) |
|
|
64
|
+
| template uses of a directive | legacy-js | 409 | 289 | 1.42 | 100% (5/5) |
|
|
65
|
+
| refs of an untyped CommonJS member | legacy-js | 387 | 401 | 0.96 | 100% (5/5) |
|
|
66
|
+
|
|
67
|
+
- def of a class method is over 0.3 - the class file has 36 lines, so the grep side reads all of it once.
|
|
68
|
+
- impact of a signature change is over 0.3 - the followed hint repeats the first output with the extra rows (refs on the changed function); impact alone is 0.33.
|
|
69
|
+
- template uses of a directive is over 0.3 - the followed hint repeats the first output with the extra rows (--grep); the two matching files have 21 and 4 lines.
|
|
70
|
+
- refs of an untyped CommonJS member is over 0.3 - the followed hint repeats the first output with the extra rows (--grep); the two matching files have 24 and 12 lines.
|
|
71
|
+
<!-- token-compare:end -->
|
|
72
|
+
|
|
73
|
+
The fixture files are 5 to 40 lines long, so a ±40-line read is the whole file: the cheapest case for grep, and where a followed hint repeating the first output weighs most. On a real Nx monorepo (143 projects, ~5.4k source files; `scripts/token-compare.ts`, run locally):
|
|
74
|
+
|
|
75
|
+
| task | ts-axi tokens | grep tokens | ratio | recall |
|
|
76
|
+
| --- | ---: | ---: | ---: | --- |
|
|
77
|
+
| def of a service method | 496 | 1256 | 0.40 | 1/1 |
|
|
78
|
+
| callers of a service method (96 call sites) | 6587 | 54314 | 0.12 | 96/96 |
|
|
79
|
+
| refs of a service method with a common name | 4653 | 147944 | 0.03 | 77/77 (244 more hits are other declarations of the name) |
|
|
80
|
+
| impact of a two-commit fix (4 changed symbols) | 2255 | 23948 | 0.09 | 44/44 (5 in the diff) |
|
|
81
|
+
|
|
82
|
+
The ts-axi side follows the `--limit` hint when a list is cut at 50 rows, as an agent would. Most rows past the first 50 are member accesses through a deep-mock mapped type (`DeepMocked<Service>`) in test files: `findReferences` returns them as extra groups defined at the method itself, and they count as References (before 0.1.15 they were dropped, and recall was 17/96 and 18/77).
|
|
83
|
+
|
|
53
84
|
## Install
|
|
54
85
|
|
|
55
86
|
Requires Node.js 20+.
|
|
@@ -274,7 +305,7 @@ One dependency: the `typescript` compiler API (ADR in `docs/adr/0001-typescript-
|
|
|
274
305
|
- `refs`, `callers` and `impact` build a `LanguageService` over the nearest `tsconfig.json` (or an implicit project of every source file when none exists, with a warning; in a monorepo or an implicit project of more than 3000 files, over the importer subgraph described above) and ask it for references and call hierarchy. Symbol identity comes from the checker, so `user.save` resolves to `User.save` through imports and re-exports.
|
|
275
306
|
- `impact` reads the "before" version of touched files with `git show` and compares signatures to classify each change (`added`, `removed`, `signature`, `body`, `type`, `modified`). Uses it cannot follow (access through `any`, dynamic string keys) are reported as `blind_spots`, never silently dropped.
|
|
276
307
|
- Module resolution keeps TypeScript's rules with two substitutions so References reach implementations: a `.d.ts` that has a JavaScript twin in the Project (`index.d.ts` beside `index.js`) resolves to the twin, and a CommonJS shim whose only statement is `module.exports = require('./x')` resolves to `./x`, so `require('..')` reaches the real function. A `.d.ts` twin that still ends up in the program is queried too and its References merged.
|
|
277
|
-
- Script-style JavaScript (AngularJS registrations and ui-router states, Meteor methods and helpers, `module.exports` objects, prototype methods, IIFE helpers) gets nested declarations with qualified names, so `outline` lists them and `impact` names the function an edit touches instead of a file-level change.
|
|
308
|
+
- Script-style JavaScript (AngularJS registrations with their `this` / `var self = this` members and factory objects, ui-router states, Meteor methods and helpers, `module.exports` objects, prototype methods, IIFE helpers) gets nested declarations with qualified names, so `outline` lists them and `impact` names the function an edit touches instead of a file-level change.
|
|
278
309
|
- Type errors in the analysed project never block anything; they are reported as a `diagnostics` count.
|
|
279
310
|
|
|
280
311
|
Every semantic command starts cold and prints `elapsed_ms`.
|
|
@@ -259,7 +259,8 @@ export function resolvePosition(index, cwd, target) {
|
|
|
259
259
|
return start >= lineStart && start < lineEnd;
|
|
260
260
|
});
|
|
261
261
|
const decls = index.declarations(abs);
|
|
262
|
-
|
|
262
|
+
// `self.loadItem = loadItem`: the member name there stands for the nested function's member too.
|
|
263
|
+
const declFor = (id) => decls.find((d) => d.namePos === id.getStart(sf)) ?? decls.find((d) => d.aliasPos === id.getStart(sf));
|
|
263
264
|
const pick = (id) => {
|
|
264
265
|
const decl = declFor(id);
|
|
265
266
|
return decl ? { kind: "decl", decl, abs } : { kind: "ident", abs, pos: id.getStart(sf), name: id.text };
|
|
@@ -251,7 +251,9 @@ function walkExtra(dir, outFiles) {
|
|
|
251
251
|
* a name in `localTo` (name → absolute file) is searched in that file only.
|
|
252
252
|
*/
|
|
253
253
|
export function textualMatches(opened, semantic, names, known, skipLines, changeSet, localTo = new Map()) {
|
|
254
|
-
|
|
254
|
+
// The whole Project, like `textualCount`: a subgraph program misses DI uses, which import nothing.
|
|
255
|
+
const byRel = new Map([...semantic.projectFiles, ...opened.index.files].map((f) => [opened.index.rel(f), f]));
|
|
256
|
+
const files = [...byRel.values()];
|
|
255
257
|
const projectSet = new Set(files);
|
|
256
258
|
walkExtra(opened.root, files);
|
|
257
259
|
sourceFs().prefetch(files);
|
|
@@ -262,7 +264,7 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
|
|
|
262
264
|
continue;
|
|
263
265
|
const rel = opened.index.rel(abs);
|
|
264
266
|
// A Declaration of another Symbol with the same name is provably not a use.
|
|
265
|
-
const declared = new Set(projectSet.has(abs) ? opened.index.declarations(abs).
|
|
267
|
+
const declared = new Set(projectSet.has(abs) ? opened.index.declarations(abs).flatMap((d) => (d.aliasLine ? [d.nameLine, d.aliasLine] : [d.nameLine])) : []);
|
|
266
268
|
const lines = new Set();
|
|
267
269
|
for (const { text: name, token } of names) {
|
|
268
270
|
if (localTo.has(name) && localTo.get(name) !== abs)
|
|
@@ -317,6 +319,10 @@ export function textualNeedle(subject, kind) {
|
|
|
317
319
|
}
|
|
318
320
|
return { text: symbol.split(".").pop(), token: false };
|
|
319
321
|
}
|
|
322
|
+
/** A Reference on a member's aliasing line (`self.x = x`) is part of its Declaration, not a use. */
|
|
323
|
+
export function onAliasLine(decl, e) {
|
|
324
|
+
return decl?.aliasLine !== undefined && e.file === decl.file && e.line === decl.aliasLine;
|
|
325
|
+
}
|
|
320
326
|
/** Whether `any`-typed property accesses of the name can be uses of the Symbol: same rule as `isTextualCandidate`. */
|
|
321
327
|
export function isBlindSpotCandidate(decl) {
|
|
322
328
|
return decl?.injected !== true;
|
|
@@ -407,7 +413,7 @@ export function textualCount(opened, needle, changeSet = new Map(), onlyFile) {
|
|
|
407
413
|
if (text === undefined || !text.includes(name))
|
|
408
414
|
continue;
|
|
409
415
|
const rel = opened.index.rel(abs);
|
|
410
|
-
const declared = new Set(project.has(abs) ? opened.index.declarations(abs).filter((d) => d.name === name).
|
|
416
|
+
const declared = new Set(project.has(abs) ? opened.index.declarations(abs).filter((d) => d.name === name).flatMap((d) => (d.aliasLine ? [d.nameLine, d.aliasLine] : [d.nameLine])) : []);
|
|
411
417
|
const n = wordMatches(text, name, token).filter((m) => !declared.has(m.line) && !inRanges(changeSet.get(rel), m.line)).length;
|
|
412
418
|
matches += n;
|
|
413
419
|
if (n > 0)
|
|
@@ -683,7 +689,7 @@ export async function impactCommand(args, ctx) {
|
|
|
683
689
|
}
|
|
684
690
|
for (const e of entries)
|
|
685
691
|
known.add(`${e.file}:${e.line}`);
|
|
686
|
-
row.all = entries.filter((e) => !e.isDefinition);
|
|
692
|
+
row.all = entries.filter((e) => !e.isDefinition && !onAliasLine(row.live, e));
|
|
687
693
|
row.outside = row.all.filter((e) => !inRanges(changeSet.get(e.file), e.line)).sort(byPosition);
|
|
688
694
|
}
|
|
689
695
|
for (const row of rows) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, sep } from "node:path";
|
|
2
2
|
import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
|
|
3
3
|
import { importerClosure } from "../importers.js";
|
|
4
|
-
import { isPlainLocal, isTextualCandidate, isUntyped, textualCount, textualNeedle, textualHint, textualLine, textualMatches, textualScan, textualScanHint } from "./impact.js";
|
|
4
|
+
import { isPlainLocal, isTextualCandidate, onAliasLine, isUntyped, textualCount, textualNeedle, textualHint, textualLine, textualMatches, textualScan, textualScanHint } from "./impact.js";
|
|
5
5
|
import { findOutsideProject, nearestTsconfig } from "../project.js";
|
|
6
6
|
import { viaLine } from "../references.js";
|
|
7
7
|
import { block, helpBlock, out } from "../render.js";
|
|
@@ -289,7 +289,7 @@ function mergedRows(direct, via, show) {
|
|
|
289
289
|
}
|
|
290
290
|
function renderRefs(semantic, opened, resolved, header, o) {
|
|
291
291
|
const split = semantic.referenceSplit(resolved.absFile, resolved.pos);
|
|
292
|
-
const rows = sortRows(
|
|
292
|
+
const rows = sortRows(split.direct.filter((e) => !onAliasLine(o.decl, e)));
|
|
293
293
|
const via = sortRows([...split.via]);
|
|
294
294
|
const impls = sortRows([...split.implementations]);
|
|
295
295
|
const { files, tests, fileList } = summary(opened, rows);
|
package/dist/src/references.js
CHANGED
|
@@ -7,7 +7,8 @@ const RANK = { direct: 0, declaration: 1, implementation: 2, via: 3, dropped: 4
|
|
|
7
7
|
* Rules: the target group is direct. When the target is a class/interface member, a group it
|
|
8
8
|
* implements/overrides (a base) is via, minus Declarations and `super` calls; a group that
|
|
9
9
|
* implements/overrides the target is an implementation (its Declaration) or via (its uses);
|
|
10
|
-
* any other member group is a sibling and dropped. Non-member groups (import aliases)
|
|
10
|
+
* any other member group is a sibling and dropped. Non-member groups (import aliases) and groups
|
|
11
|
+
* defined at the target itself (mock mapped types) are direct.
|
|
11
12
|
*/
|
|
12
13
|
export function classifyReferenceGroups(groups, targetIndex) {
|
|
13
14
|
const target = groups[targetIndex];
|
|
@@ -19,7 +20,8 @@ export function classifyReferenceGroups(groups, targetIndex) {
|
|
|
19
20
|
};
|
|
20
21
|
const bases = new Set(target?.bases ?? []);
|
|
21
22
|
groups.forEach((group, i) => {
|
|
22
|
-
|
|
23
|
+
// Mock shapes (`{ [K in keyof T]: … } & T`) come back as extra groups defined at the target itself.
|
|
24
|
+
const isTarget = i === targetIndex || (target !== undefined && group.key === target.key);
|
|
23
25
|
const relation = isTarget || !target?.member || !group.member
|
|
24
26
|
? "direct"
|
|
25
27
|
: bases.has(group.key)
|
package/dist/src/symbols.js
CHANGED
|
@@ -289,27 +289,57 @@ function functionByIdentifier(call, arg) {
|
|
|
289
289
|
let scope = call;
|
|
290
290
|
while (!ts.isSourceFile(scope) && !(ts.isBlock(scope) && ts.isFunctionLike(scope.parent)))
|
|
291
291
|
scope = scope.parent;
|
|
292
|
-
|
|
292
|
+
return functionInStatements(scope.statements, ref.text);
|
|
293
|
+
}
|
|
294
|
+
/** The function declaration or function-valued variable `name` among `statements` (any position: hoisting), with its `$inject`. */
|
|
295
|
+
function functionInStatements(statements, name) {
|
|
293
296
|
let found;
|
|
294
297
|
let inject;
|
|
295
298
|
for (const st of statements) {
|
|
296
|
-
if (ts.isFunctionDeclaration(st) && st.name?.text ===
|
|
299
|
+
if (ts.isFunctionDeclaration(st) && st.name?.text === name && st.body)
|
|
297
300
|
found = { fn: st, name: st.name };
|
|
298
301
|
else if (ts.isVariableStatement(st)) {
|
|
299
302
|
for (const d of st.declarationList.declarations) {
|
|
300
303
|
const fn = asFunction(d.initializer);
|
|
301
|
-
if (fn && ts.isIdentifier(d.name) && d.name.text ===
|
|
304
|
+
if (fn && ts.isIdentifier(d.name) && d.name.text === name)
|
|
302
305
|
found = { fn, name: d.name };
|
|
303
306
|
}
|
|
304
307
|
}
|
|
305
308
|
else if (ts.isExpressionStatement(st) && ts.isBinaryExpression(st.expression) && ts.isPropertyAccessExpression(st.expression.left)) {
|
|
306
309
|
const { left, right } = st.expression;
|
|
307
|
-
if (left.name.text === "$inject" && ts.isIdentifier(left.expression) && left.expression.text ===
|
|
310
|
+
if (left.name.text === "$inject" && ts.isIdentifier(left.expression) && left.expression.text === name && ts.isArrayLiteralExpression(right))
|
|
308
311
|
inject = right;
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
return found && (inject ? { ...found, inject } : found);
|
|
312
315
|
}
|
|
316
|
+
/** Names bound to `this` at the top level of a body (`var self = this`, `const vm = this`). */
|
|
317
|
+
function thisAliases(statements) {
|
|
318
|
+
const names = new Set();
|
|
319
|
+
for (const st of statements) {
|
|
320
|
+
if (!ts.isVariableStatement(st))
|
|
321
|
+
continue;
|
|
322
|
+
for (const d of st.declarationList.declarations) {
|
|
323
|
+
if (ts.isIdentifier(d.name) && d.initializer && unwrap(d.initializer).kind === ts.SyntaxKind.ThisKeyword)
|
|
324
|
+
names.add(d.name.text);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return names;
|
|
328
|
+
}
|
|
329
|
+
/** The object literal of a top-level `var <name> = {...}` among `statements`. */
|
|
330
|
+
function objectVariable(statements, name) {
|
|
331
|
+
let found;
|
|
332
|
+
for (const st of statements) {
|
|
333
|
+
if (!ts.isVariableStatement(st))
|
|
334
|
+
continue;
|
|
335
|
+
for (const d of st.declarationList.declarations) {
|
|
336
|
+
const init = d.initializer && unwrap(d.initializer);
|
|
337
|
+
if (ts.isIdentifier(d.name) && d.name.text === name && init && ts.isObjectLiteralExpression(init))
|
|
338
|
+
found = init;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return found;
|
|
342
|
+
}
|
|
313
343
|
function isStringName(node) {
|
|
314
344
|
return node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node));
|
|
315
345
|
}
|
|
@@ -363,8 +393,21 @@ export function collectDeclarations(sf, file) {
|
|
|
363
393
|
const nested = (raw) => {
|
|
364
394
|
raws.push({ ...raw, qualifiedName: raw.qualifiedName ?? (raw.parentName ? `${raw.parentName}.${raw.name}` : raw.name), hasBody: true });
|
|
365
395
|
};
|
|
366
|
-
/**
|
|
367
|
-
|
|
396
|
+
/**
|
|
397
|
+
* `<holder>.<name>` for a nested function named by identifier (`self.x = loadItem`, `{ x: loadItem }`): the
|
|
398
|
+
* function's own span, signature and name, so its line and the aliasing line both resolve to the member.
|
|
399
|
+
*/
|
|
400
|
+
const aliasedMember = (scope, ref, name, holder, exported, alias) => {
|
|
401
|
+
const target = functionInStatements(scope, ref.text);
|
|
402
|
+
if (!target)
|
|
403
|
+
return;
|
|
404
|
+
const decl = ts.isFunctionDeclaration(target.fn) ? target.fn : target.name.parent;
|
|
405
|
+
const list = decl.parent;
|
|
406
|
+
const node = ts.isVariableDeclaration(decl) && ts.isVariableDeclarationList(list) && ts.isVariableStatement(list.parent) && list.declarations[0] === decl ? list.parent : decl;
|
|
407
|
+
nested({ start: node.getStart(sf), end: node.end, kind: "method", name, parentName: holder, exported, signature: fullSignatureOf(decl, sf), namePos: target.name.getStart(sf), aliasPos: alias.getStart(sf) });
|
|
408
|
+
};
|
|
409
|
+
/** Method and function-valued properties of an object literal as `<holder>.<name>`; with `scope`, also identifiers naming its functions. */
|
|
410
|
+
const objectMembers = (obj, holder, exported, scope) => {
|
|
368
411
|
for (const prop of obj.properties) {
|
|
369
412
|
const name = nameText(prop.name, sf);
|
|
370
413
|
if (!name || !prop.name)
|
|
@@ -374,35 +417,60 @@ export function collectDeclarations(sf, file) {
|
|
|
374
417
|
}
|
|
375
418
|
else if (ts.isPropertyAssignment(prop)) {
|
|
376
419
|
const fn = asFunction(prop.initializer);
|
|
420
|
+
const value = unwrap(prop.initializer);
|
|
377
421
|
if (fn)
|
|
378
422
|
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
|
+
else if (scope && ts.isIdentifier(value))
|
|
424
|
+
aliasedMember(scope, value, name, holder, exported, prop.name);
|
|
425
|
+
}
|
|
426
|
+
else if (scope && ts.isShorthandPropertyAssignment(prop)) {
|
|
427
|
+
aliasedMember(scope, prop.name, name, holder, exported, prop.name);
|
|
379
428
|
}
|
|
380
429
|
}
|
|
381
430
|
};
|
|
382
|
-
/**
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
return;
|
|
386
|
-
for (const statement of body.statements) {
|
|
431
|
+
/** `<holder>.x = function | <nested function>` at the top level of a body; `isHolder` matches `this`, its aliases or a returned object. */
|
|
432
|
+
const memberAssignments = (statements, isHolder, owner, exported) => {
|
|
433
|
+
for (const statement of statements) {
|
|
387
434
|
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
|
|
388
435
|
continue;
|
|
389
436
|
const { left, operatorToken, right } = statement.expression;
|
|
390
|
-
if (operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isPropertyAccessExpression(left) || left.expression
|
|
437
|
+
if (operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isPropertyAccessExpression(left) || !isHolder(left.expression))
|
|
391
438
|
continue;
|
|
392
439
|
const value = unwrap(right);
|
|
393
440
|
if (ts.isObjectLiteralExpression(value))
|
|
394
441
|
objectMembers(value, `${owner}.${left.name.text}`, exported);
|
|
442
|
+
if (ts.isIdentifier(value))
|
|
443
|
+
aliasedMember(statements, value, left.name.text, owner, exported, left.name);
|
|
395
444
|
const fn = asFunction(right);
|
|
396
445
|
if (!fn)
|
|
397
446
|
continue;
|
|
398
447
|
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) });
|
|
399
448
|
}
|
|
400
449
|
};
|
|
401
|
-
/**
|
|
450
|
+
/** `this.x = ...` (also through `var self = this`) at the top level of a constructor-like body. */
|
|
451
|
+
const thisMembers = (body, owner, exported) => {
|
|
452
|
+
if (!body || !ts.isBlock(body))
|
|
453
|
+
return;
|
|
454
|
+
const aliases = thisAliases(body.statements);
|
|
455
|
+
memberAssignments(body.statements, (e) => e.kind === ts.SyntaxKind.ThisKeyword || (ts.isIdentifier(e) && aliases.has(e.text)), owner, exported);
|
|
456
|
+
};
|
|
457
|
+
/** Object literals a function returns at its top level (a factory's API, a deps object), directly or as `var service = {...}; return service`. */
|
|
402
458
|
const returnedMembers = (body, owner, exported) => {
|
|
403
|
-
if (body)
|
|
404
|
-
|
|
405
|
-
|
|
459
|
+
if (!body)
|
|
460
|
+
return;
|
|
461
|
+
const scope = ts.isBlock(body) ? body.statements : undefined;
|
|
462
|
+
for (const obj of returnedObjects(body))
|
|
463
|
+
objectMembers(obj, owner, exported, scope);
|
|
464
|
+
for (const st of scope ?? []) {
|
|
465
|
+
const ret = ts.isReturnStatement(st) && st.expression ? unwrap(st.expression) : undefined;
|
|
466
|
+
if (!ret || !ts.isIdentifier(ret))
|
|
467
|
+
continue;
|
|
468
|
+
const obj = objectVariable(scope, ret.text);
|
|
469
|
+
if (!obj)
|
|
470
|
+
continue;
|
|
471
|
+
objectMembers(obj, owner, exported, scope);
|
|
472
|
+
memberAssignments(scope, (e) => ts.isIdentifier(e) && e.text === ret.text, owner, exported);
|
|
473
|
+
}
|
|
406
474
|
};
|
|
407
475
|
const topLevelExported = (name) => raws.find((r) => r.qualifiedName === name && r.parentName === undefined)?.exported ?? false;
|
|
408
476
|
const isConstructorName = (name) => /^[A-Z]/.test(name);
|
|
@@ -851,7 +919,18 @@ export function collectDeclarations(sf, file) {
|
|
|
851
919
|
const kept = registeredBodies.length === 0
|
|
852
920
|
? raws
|
|
853
921
|
: raws.filter((r) => r.registered || !registeredBodies.some(([s, e]) => r.namePos >= s && r.namePos < e));
|
|
854
|
-
|
|
922
|
+
// One function named twice for the same holder (`this.x = x` and `return { x }`) is one member.
|
|
923
|
+
const seen = new Set();
|
|
924
|
+
const unique = kept.filter((r) => {
|
|
925
|
+
if (r.aliasPos === undefined)
|
|
926
|
+
return true;
|
|
927
|
+
const key = `${r.qualifiedName}@${r.namePos}`;
|
|
928
|
+
if (seen.has(key))
|
|
929
|
+
return false;
|
|
930
|
+
seen.add(key);
|
|
931
|
+
return true;
|
|
932
|
+
});
|
|
933
|
+
const ordered = unique.map((r, i) => ({ r, i })).sort((a, b) => a.r.start - b.r.start || a.i - b.i);
|
|
855
934
|
return ordered.map(({ r }) => {
|
|
856
935
|
const startLc = sf.getLineAndCharacterOfPosition(r.start);
|
|
857
936
|
const full = r.signature;
|
|
@@ -872,6 +951,7 @@ export function collectDeclarations(sf, file) {
|
|
|
872
951
|
fullSignature: full,
|
|
873
952
|
...(r.nameless ? { nameless: true } : {}),
|
|
874
953
|
...(r.injected ? { injected: true } : {}),
|
|
954
|
+
...(r.aliasPos !== undefined ? { aliasPos: r.aliasPos, aliasLine: sf.getLineAndCharacterOfPosition(r.aliasPos).line + 1 } : {}),
|
|
875
955
|
};
|
|
876
956
|
});
|
|
877
957
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-axi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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": [
|