ts-axi 0.1.11 → 0.1.12

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 CHANGED
@@ -78,7 +78,7 @@ The hook runs `ts-axi` with no arguments: it reads only the file tree and git st
78
78
  | Command | What it does |
79
79
  | --- | --- |
80
80
  | `ts-axi` | Home view: project root, tsconfig used (or implicit), source file count, test-file glob and match counts, git branch and dirty count, next steps. Under a second, exit 0 everywhere |
81
- | `def <target>` | Declarations of a symbol by name (`save`, `User.save`, `path:line`): one match prints detail and body, several print a compact list, none prints `did_you_mean` |
81
+ | `def <target>` | Declarations of a symbol by name (`save`, `User.save`, `path:line`): one match prints detail and body, several print a compact list, none prints `did_you_mean` and, when the name still occurs in the code, the `refs <name> --grep` that lists those textual matches |
82
82
  | `refs <target>` | References of one symbol, grouped by file, with test counts. A class member that implements an interface (or overrides a base) member counts only its direct References; calls through the interface are `via_interface: n (Iface.member)`, sibling implementations are dropped, and an interface member reports `implementations: n`. `--limit`, `--kind calls`, `--via` (via rows with a `via` column), `--implementations` |
83
83
  | `callers <target>` | Incoming calls of a function-like symbol, split the same way (`via_interface`, `--via`). `--depth 1..3` |
84
84
  | `outline <file>` | Top-level declarations in source order, class members nested: `{line,kind,name,signature}` (`{at,kind,name,signature}` over several files). `--exports`, `--fields exported` |
@@ -180,7 +180,7 @@ decls[4]{line,kind,name,exported,signature}:
180
180
  17,function,cartBadge,true,directive cartBadge()
181
181
  ```
182
182
 
183
- An edit inside `CartCtrl` is reported by `impact` as `CartCtrl body`, not as a file-level change. Names registered by a string have no identifier the checker can follow; `refs` says so and offers `--grep`. ui-router and ngRoute states are Declarations of kind `state` (`states.cart`, with callbacks as `states.cart.resolve.items`), so an edit inside a `resolve` names that callback; state objects in exported arrays (`` module.exports = [{ name: `${base}.init`, url, resolve }] ``) count too, their template-literal names filled from same-file consts. Directives, components, their `scope`/`bindings` keys, controllers and filters also get `templates: <n> usages in <m> files` (kebab-case elements and attributes, `ng-controller`, `| filter` in `.html`/`.htm`/`.ejs`/`.hbs`/`.njk`/`.pug`), a `templates` column in `impact`, and a changed template that uses a changed directive or binding is listed under `template_changes` instead of `skipped`.
183
+ An edit inside `CartCtrl` is reported by `impact` as `CartCtrl body`, not as a file-level change. Names registered by a string have no identifier the checker can follow; `refs` says so and offers `--grep`. A registration that names a same-file function (`.directive('fancyBox', fancyBox)`, `['$uibModal', fancyBox]`) links the two, so `refs` on the function reports the registration's template usages too. Function-valued members of an object a function returns (`return { canReadItem: async () => ... }`) are Declarations (`makeItemDeps.canReadItem`), so `def canReadItem` finds them; their `textual:` count covers the whole project, while a real local (a parameter, an IIFE helper, a CommonJS binding no `module.exports` hands out) says `in this file`. ui-router and ngRoute states are Declarations of kind `state` (`states.cart`, with callbacks as `states.cart.resolve.items`), so an edit inside a `resolve` names that callback; state objects in exported arrays (`` module.exports = [{ name: `${base}.init`, url, resolve }] ``) count too, their template-literal names filled from same-file consts. Directives, components, their `scope`/`bindings` keys, controllers and filters also get `templates: <n> usages in <m> files` (kebab-case elements and attributes, `ng-controller`, `| filter` in `.html`/`.htm`/`.ejs`/`.hbs`/`.njk`/`.pug`), a `templates` column in `impact`, and a changed template that uses a changed directive or binding is listed under `template_changes` instead of `skipped`.
184
184
 
185
185
  ## Monorepos
186
186
 
@@ -217,9 +217,27 @@ function identifiers(node, sf, outIds) {
217
217
  }
218
218
  ts.forEachChild(node, (child) => identifiers(child, sf, outIds));
219
219
  }
220
+ function isFunctionValue(expr) {
221
+ let e = expr;
222
+ while (ts.isParenthesizedExpression(e))
223
+ e = e.expression;
224
+ return ts.isArrowFunction(e) || ts.isFunctionExpression(e);
225
+ }
226
+ /** Declares a name: a Declaration kind, a parameter, a binding element, or a function-valued property. */
220
227
  function isDeclarationName(id) {
221
228
  const parent = id.parent;
222
- return parent !== undefined && parent.name === id && (kindOf(parent) !== undefined || ts.isParameter(parent) || ts.isBindingElement(parent));
229
+ if (parent === undefined || parent.name !== id)
230
+ return false;
231
+ if (ts.isPropertyAssignment(parent))
232
+ return isFunctionValue(parent.initializer);
233
+ return kindOf(parent) !== undefined || ts.isParameter(parent) || ts.isBindingElement(parent);
234
+ }
235
+ /** The node a declaration name owns, for nesting: `this.x = fn` → the whole assignment. */
236
+ function declarationNode(id) {
237
+ const parent = id.parent;
238
+ if (ts.isPropertyAccessExpression(parent) && parent.name === id && ts.isBinaryExpression(parent.parent) && parent.parent.left === parent)
239
+ return parent.parent;
240
+ return parent;
223
241
  }
224
242
  /** The identifier a `path:line[:col]` target points at (see PLAN.md Targets). */
225
243
  export function resolvePosition(index, cwd, target) {
@@ -252,33 +270,41 @@ export function resolvePosition(index, cwd, target) {
252
270
  return hit ? pick(hit) : { kind: "none", abs, file, reason: `no identifier at ${file}:${target.line}:${target.col}` };
253
271
  }
254
272
  // A string-registered name has no identifier: its line stands for it.
255
- const nameless = decls.find((d) => d.nameless && d.nameLine === target.line);
273
+ // The registration line too: `.directive('x', fancyBox)` has its name far from the function.
274
+ const nameless = decls.find((d) => d.nameless && d.nameLine === target.line) ?? decls.find((d) => d.nameless && d.line === target.line);
256
275
  if (nameless)
257
276
  return { kind: "decl", decl: nameless, abs };
258
277
  if (onLine.length === 0)
259
278
  return { kind: "none", abs, file, reason: `no identifier on ${file}:${target.line}` };
279
+ const ambiguous = (ids) => {
280
+ const distinct = new Map();
281
+ for (const id of ids)
282
+ if (!distinct.has(id.text))
283
+ distinct.set(id.text, id);
284
+ if (distinct.size === 1)
285
+ return pick(ids[0]);
286
+ return {
287
+ kind: "ambiguous",
288
+ abs,
289
+ file,
290
+ candidates: [...distinct.values()].map((id) => ({ name: id.text, line: target.line, col: id.getStart(sf) - lineStart + 1 })),
291
+ };
292
+ };
260
293
  const constructorDecl = decls.find((d) => d.nameLine === target.line && d.name === "constructor");
261
- const declared = onLine.find((id) => declFor(id)) ?? onLine.find(isDeclarationName);
262
- if (declared)
263
- return pick(declared);
294
+ // The declaration that starts the line: names nested in another one's declaration (its parameters, its members) rank after it.
295
+ const declaring = onLine.filter((id) => declFor(id) !== undefined || isDeclarationName(id));
296
+ const owned = (inner, outer) => {
297
+ const node = declarationNode(outer);
298
+ const at = inner.getStart(sf);
299
+ return node !== declarationNode(inner) && node.getStart(sf) <= at && at < node.end;
300
+ };
301
+ const outer = declaring.filter((id) => !(constructorDecl && ts.isParameter(id.parent) && ts.isConstructorDeclaration(id.parent.parent)) &&
302
+ !declaring.some((other) => other !== id && owned(id, other)));
303
+ if (outer.length > 0)
304
+ return ambiguous(outer);
264
305
  if (constructorDecl)
265
306
  return { kind: "decl", decl: constructorDecl, abs };
266
- const distinct = new Map();
267
- for (const id of onLine)
268
- if (!distinct.has(id.text))
269
- distinct.set(id.text, id);
270
- if (distinct.size === 1)
271
- return pick(onLine[0]);
272
- return {
273
- kind: "ambiguous",
274
- abs,
275
- file,
276
- candidates: [...distinct.values()].map((id) => ({
277
- name: id.text,
278
- line: target.line,
279
- col: id.getStart(sf) - lineStart + 1,
280
- })),
281
- };
307
+ return ambiguous(onLine);
282
308
  }
283
309
  /** A Declaration for an arbitrary declaration name position (locals, parameters) not in the index. */
284
310
  export function declarationAt(index, abs, pos, fallbackKind) {
@@ -1,5 +1,6 @@
1
1
  import { bool, expectPositionals, flag, parseArgs } from "../args.js";
2
2
  import { semanticFor } from "../scope.js";
3
+ import { textualScan, textualScanHint } from "./impact.js";
3
4
  import { block, fenced, helpBlock, out } from "../render.js";
4
5
  import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
5
6
  import { BODY_LINES, declarationAt, filterSuffix, MATCH_ROWS, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, useRef, } from "./common.js";
@@ -9,7 +10,7 @@ flags[8]:
9
10
  output:
10
11
  1 match = file, line, kind, exported, signature + body fenced with file line numbers
11
12
  several = matches[n]{at,kind,signature} (at = path:line) (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms (--in, --kind, Class.member, --exported) with how many each keeps
12
- none = matches: 0 + did_you_mean
13
+ none = matches: 0 + did_you_mean; when the name still occurs in Project files, help gives the refs --grep that lists those textual matches
13
14
  examples[5]:
14
15
  ts-axi def User
15
16
  ts-axi def User.save
@@ -78,7 +79,8 @@ export async function defCommand(args, ctx) {
78
79
  return detail(opened, groups[0].decls, full, label);
79
80
  if (groups.length === 0) {
80
81
  const suggested = suggestions(opened, target);
81
- const help = suggested.slice(0, 2).map((s) => `Run \`ts-axi def ${s}\``);
82
+ const text = f.fuzzy ? 0 : textualScan(opened, target.member ?? target.name).length;
83
+ const help = [...(text > 0 ? [textualScanHint(label, text)] : []), ...suggested.slice(0, 2).map((s) => `Run \`ts-axi def ${s}\``)];
82
84
  if (!f.fuzzy)
83
85
  help.push(`Run \`ts-axi def ${label} --fuzzy\` for substring matches`);
84
86
  if (f.kind || f.in.length > 0 || f.exported)
@@ -270,6 +270,27 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
270
270
  }
271
271
  return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
272
272
  }
273
+ /** Whole-word matches of `name` in the Project files, one row per line: the fallback when no Declaration matches. */
274
+ export function textualScan(opened, name) {
275
+ const rows = [];
276
+ sourceFs().prefetch(opened.index.files);
277
+ for (const abs of opened.index.files) {
278
+ const text = opened.index.text(abs);
279
+ if (text === undefined || !text.includes(name))
280
+ continue;
281
+ let last = 0;
282
+ for (const m of wordMatches(text, name)) {
283
+ if (m.line === last)
284
+ continue;
285
+ last = m.line;
286
+ rows.push({ file: opened.index.rel(abs), line: m.line, context: contextOf(lineOf(text, m.line), m.col) });
287
+ }
288
+ }
289
+ return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
290
+ }
291
+ export function textualScanHint(label, matches) {
292
+ return `Run \`ts-axi refs ${label} --grep\` for ${matches} textual matches`;
293
+ }
273
294
  /** Whether a whole-word count of the name says anything: not for router-called or by-name-injected members (`injected`). */
274
295
  export function isTextualCandidate(decl) {
275
296
  return decl?.injected !== true;
@@ -297,9 +318,66 @@ export function isUntyped(opened, decl, file) {
297
318
  return false;
298
319
  return decl?.nameless === true || opened.index.project.implicit || (file !== undefined && /\.[mc]?jsx?$/.test(file));
299
320
  }
300
- /** A non-exported, non-member, identifier-named Declaration: only its own file can name it. */
301
- export function isPlainLocal(decl) {
302
- return decl !== undefined && !decl.exported && !decl.nameless && decl.parentName === undefined;
321
+ /** The identifier starting at `pos`. */
322
+ function identifierAt(node, pos, sf) {
323
+ if (ts.isIdentifier(node) && node.getStart(sf) === pos)
324
+ return node;
325
+ return ts.forEachChild(node, (child) => (child.pos <= pos && pos < child.end ? identifierAt(child, pos, sf) : undefined));
326
+ }
327
+ /** Names a CommonJS file hands out (`module.exports = { a, b: c }`, `exports.x = d`); undefined when not CommonJS. */
328
+ function commonJsExports(sf) {
329
+ const names = new Set();
330
+ let cjs = /\brequire\s*\(/.test(sf.text);
331
+ // Values only: identifiers used inside exported function bodies do not leave the file.
332
+ const collect = (expr) => {
333
+ let e = expr;
334
+ while (ts.isParenthesizedExpression(e))
335
+ e = e.expression;
336
+ if (ts.isIdentifier(e))
337
+ names.add(e.text);
338
+ else if (ts.isObjectLiteralExpression(e)) {
339
+ for (const p of e.properties) {
340
+ if (ts.isShorthandPropertyAssignment(p))
341
+ names.add(p.name.text);
342
+ else if (ts.isPropertyAssignment(p))
343
+ collect(p.initializer);
344
+ else if (ts.isSpreadAssignment(p))
345
+ collect(p.expression);
346
+ }
347
+ }
348
+ };
349
+ for (const st of sf.statements) {
350
+ if (!ts.isExpressionStatement(st) || !ts.isBinaryExpression(st.expression) || st.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
351
+ continue;
352
+ const left = st.expression.left.getText(sf).replace(/\s+/g, "");
353
+ if (!/^(module\.exports|exports)(\.|$)/.test(left))
354
+ continue;
355
+ cjs = true;
356
+ collect(st.expression.right);
357
+ }
358
+ return cjs ? names : undefined;
359
+ }
360
+ /**
361
+ * A Declaration only its own file can name: a parameter or function-scoped binding, or a non-exported top-level
362
+ * binding of an ES module, or of a CommonJS file whose exports never mention it. Members, object properties,
363
+ * string registrations and script globals are not.
364
+ */
365
+ export function isPlainLocal(index, decl) {
366
+ if (decl === undefined || decl.exported || decl.nameless || decl.parentName !== undefined)
367
+ return false;
368
+ const sf = index.sourceFile(index.abs(decl.file));
369
+ const id = sf && identifierAt(sf, decl.namePos, sf);
370
+ if (!sf || !id)
371
+ return false;
372
+ const owner = id.parent;
373
+ if (ts.isPropertyAssignment(owner) || ts.isShorthandPropertyAssignment(owner) || ts.isMethodDeclaration(owner) || ts.isPropertyAccessExpression(owner))
374
+ return false;
375
+ for (let n = owner.parent; !ts.isSourceFile(n); n = n.parent)
376
+ if (ts.isFunctionLike(n))
377
+ return true;
378
+ if (ts.isExternalModule(sf))
379
+ return true;
380
+ return commonJsExports(sf)?.has(decl.name) === false;
303
381
  }
304
382
  /**
305
383
  * Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded.
@@ -592,7 +670,7 @@ export async function impactCommand(args, ctx) {
592
670
  for (const row of rows) {
593
671
  if (!row.live || !isUntyped(opened, row.live, row.live.file))
594
672
  continue;
595
- const localTo = isPlainLocal(row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
673
+ const localTo = isPlainLocal(opened.index, row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
596
674
  const needle = textualNeedle(row.live);
597
675
  if (!needle)
598
676
  continue;
@@ -667,7 +745,7 @@ export async function impactCommand(args, ctx) {
667
745
  if (!textNames.has(needle.text))
668
746
  textNames.set(needle.text, needle);
669
747
  const abs = opened.index.abs(decl.file);
670
- if (!isPlainLocal(decl) || (localTo.has(name) && localTo.get(name) !== abs))
748
+ if (!isPlainLocal(opened.index, decl) || (localTo.has(name) && localTo.get(name) !== abs))
671
749
  wide.add(name);
672
750
  else
673
751
  localTo.set(name, abs);
@@ -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 } from "./impact.js";
4
+ import { isPlainLocal, isTextualCandidate, 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";
@@ -10,7 +10,7 @@ import { loadTemplates, templatesLine, templateTarget, templateUsages } from "..
10
10
  import { byDepthThenPath } from "../walk.js";
11
11
  import { detectWorkspace, isMultiProject } from "../workspace.js";
12
12
  import { isTestFile } from "../settings.js";
13
- import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
13
+ import { isFunctionLike, kindFromScriptElement, parseTarget, registrationOf } from "../symbols.js";
14
14
  import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, useRef, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
15
15
  const DEFAULT_LIMIT = 50;
16
16
  const MAX_LIMIT = 500;
@@ -23,8 +23,8 @@ const VIA_DOC = `via_interface: n (Iface.member) = References through an interfa
23
23
  refs_total: 0 under -P in a workspace lists found_outside_project[n]{file,line} = importers outside that Project
24
24
  scope = in a monorepo without -P the program is the declaring file + its importers (scope_partial: true when the importer scan was capped)
25
25
  tsconfig: x (n referenced configs) = a references-only tsconfig expanded to the configs it references
26
- textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' for a non-exported local; above refs_total = uses the checker cannot resolve - see refs --grep
27
- templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money); refs --grep lists them in unresolved`;
26
+ textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' only for a real local (a parameter, a binding inside a function or IIFE, a non-exported top-level binding of an ES module or of a CommonJS file whose module.exports never hands it out) - members, object properties and script globals count Project-wide; above refs_total = uses the checker cannot resolve - see refs --grep
27
+ templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money) - also for a function registered by identifier (.directive('fancyBox', fancyBox)), under the registered name; refs --grep lists them in unresolved`;
28
28
  export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
29
29
  flags[13]:
30
30
  ${SHARED_FLAGS}
@@ -34,7 +34,9 @@ flags[13]:
34
34
  output:
35
35
  symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,context} (Declarations excluded), elapsed_ms
36
36
  ${VIA_DOC}
37
- note = the name is registered by a string ('Name' in .controller('Name', fn)) - its uses are textual; add --grep
37
+ note = the name is registered by a string ('Name' in .controller('Name', fn), or fn a same-file function named by identifier) - its uses are textual; add --grep
38
+ matches: 0 = no Declaration of the name; help counts its whole-word matches in Project files, and --grep lists them in unresolved with note 'no declaration found - textual matches only'
39
+ path:line = the declaration that starts the line (a function before its parameters, a property before its value's parameters; siblings like 'const a = 1, b = 2' list identifiers - add :col)
38
40
  an ambiguous name lists matches[n] (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms; nothing is analysed
39
41
  examples[4]:
40
42
  ts-axi refs User.save
@@ -111,8 +113,23 @@ async function run(args, ctx, invoked) {
111
113
  `Run \`ts-axi ${name} ${outside[0].file}:${outside[0].nameLine}\` to use the nearest tsconfig of that file`,
112
114
  ]));
113
115
  }
116
+ // No Declaration, but the name may still be written: an untyped property set up elsewhere.
117
+ const text = f.fuzzy ? [] : textualScan(opened, target.member ?? target.name);
118
+ if (show.grep) {
119
+ return out(block({
120
+ ...warning,
121
+ matches: 0,
122
+ note: "no declaration found - textual matches only",
123
+ unresolved: text.length === 0 ? 0 : text.slice(0, limit),
124
+ ...(text.length > limit ? { unresolved_total: text.length } : {}),
125
+ }), helpBlock([
126
+ ...(text.length > limit ? [`Showing ${limit} of ${text.length}; run \`ts-axi refs ${label} --grep --limit ${Math.min(MAX_LIMIT, text.length)}\` for more`] : []),
127
+ `Run \`ts-axi def ${label} --fuzzy\` for substring matches`,
128
+ ]));
129
+ }
114
130
  const suggested = suggestions(opened, target);
115
131
  return out(block({ ...warning, matches: 0, ...(suggested.length > 0 ? { did_you_mean: suggested } : {}) }), helpBlock([
132
+ ...(text.length > 0 ? [textualScanHint(label, text.length)] : []),
116
133
  ...suggested.slice(0, 2).map((s) => `Run \`ts-axi ${name} ${s}\``),
117
134
  ...(f.fuzzy ? [] : [`Run \`ts-axi def ${label} --fuzzy\` for substring matches`]),
118
135
  ]));
@@ -143,15 +160,17 @@ async function run(args, ctx, invoked) {
143
160
  }
144
161
  const { semantic, scope } = built;
145
162
  const def = semantic.definitionAt(resolved.absFile, resolved.pos);
146
- const decl = resolved.decl ??
163
+ const found = resolved.decl ??
147
164
  (def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined);
165
+ // A function registered by identifier is its registration: templates, note and textual use the registered name.
166
+ const decl = (found && registrationOf(found, opened.index.declarations(opened.index.abs(found.file)))) ?? found;
148
167
  const kind = decl?.kind ?? kindFromScriptElement(def?.kind ?? semantic.quickInfo(resolved.absFile, resolved.pos)?.kind ?? "");
149
168
  const symbol = decl?.qualifiedName ?? def?.name ?? label;
150
169
  const declaredAt = decl ? `${decl.file}:${decl.nameLine}` : def && def.line > 0 ? `${def.file}:${def.line}` : "(outside the Project)";
151
170
  const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
152
171
  const explicit = Boolean(project || opened.settings.project);
153
172
  const declaredFile = decl?.file ?? (def && def.line > 0 ? def.file : undefined);
154
- const localFile = isPlainLocal(decl) ? opened.index.abs(decl.file) : undefined;
173
+ const localFile = isPlainLocal(opened.index, decl) ? opened.index.abs(decl.file) : undefined;
155
174
  const needle = textualNeedle(decl ?? symbol, kind);
156
175
  const textual = declaredFile !== undefined && needle && isUntyped(opened, decl, declaredFile) ? textualCount(opened, needle, new Map(), localFile) : undefined;
157
176
  const tplTarget = decl ? templateTarget(decl, opened.index.declarations(opened.index.abs(decl.file))) : undefined;
@@ -249,7 +268,7 @@ function unresolvedOf(opened, semantic, o, known) {
249
268
  const needle = isTextualCandidate(o.decl) ? textualNeedle(o.decl ?? o.symbol, o.kind) : undefined;
250
269
  if (!needle)
251
270
  return [];
252
- return textualMatches(opened, semantic, [needle], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map(), isPlainLocal(o.decl) ? new Map([[o.decl.name, opened.index.abs(o.decl.file)]]) : new Map());
271
+ return textualMatches(opened, semantic, [needle], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map(), isPlainLocal(opened.index, o.decl) ? new Map([[o.decl.name, opened.index.abs(o.decl.file)]]) : new Map());
253
272
  }
254
273
  /**
255
274
  * `textual` field and, when the name occurs more often than refs_total and some matches sit on no
@@ -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.12",
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": [