ts-axi 0.1.7 → 0.1.8

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.
@@ -242,16 +242,16 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
242
242
  const rows = [];
243
243
  for (const abs of files) {
244
244
  const text = opened.index.text(abs);
245
- if (text === undefined || !names.some((n) => text.includes(n)))
245
+ if (text === undefined || !names.some((n) => text.includes(n.text)))
246
246
  continue;
247
247
  const rel = opened.index.rel(abs);
248
248
  // A Declaration of another Symbol with the same name is provably not a use.
249
249
  const declared = new Set(projectSet.has(abs) ? opened.index.declarations(abs).map((d) => d.nameLine) : []);
250
250
  const lines = new Set();
251
- for (const name of names) {
251
+ for (const { text: name, token } of names) {
252
252
  if (localTo.has(name) && localTo.get(name) !== abs)
253
253
  continue;
254
- for (const m of wordMatches(text, name)) {
254
+ for (const m of wordMatches(text, name, token)) {
255
255
  if (known.has(`${rel}:${m.line}`) || skipLines.has(`${rel}:${m.line}`) || declared.has(m.line) || inRanges(changeSet.get(rel), m.line))
256
256
  continue;
257
257
  if (lines.has(m.line))
@@ -263,12 +263,30 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
263
263
  }
264
264
  return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
265
265
  }
266
+ /** Whether a whole-word count of the name says anything: not for router-called or by-name-injected members (`injected`). */
267
+ export function isTextualCandidate(decl) {
268
+ return decl?.injected !== true;
269
+ }
266
270
  /**
267
- * The checker may miss uses: JS declaring file, a string-registered name, or no tsconfig at all.
268
- * A state's hooks (`onEnter`, inline `controller`) are called by the router, never by name: no textual count.
271
+ * The textual needle of a Symbol: a state's full name without the `states.`/`routes.` prefix (a token, so
272
+ * `ui-sref="a.b"` counts and `a.b.c` does not match `a.b`; none when it still holds `*`), else the last
273
+ * segment of the qualified name.
269
274
  */
275
+ export function textualNeedle(subject, kind) {
276
+ const symbol = typeof subject === "string" ? subject : subject.qualifiedName;
277
+ if ((kind ?? (typeof subject === "string" ? undefined : subject.kind)) === "state") {
278
+ const name = symbol.replace(/^(states|routes)\./, "");
279
+ return name.includes("*") || name === "" ? undefined : { text: name, token: true };
280
+ }
281
+ return { text: symbol.split(".").pop(), token: false };
282
+ }
283
+ /** Whether `any`-typed property accesses of the name can be uses of the Symbol: same rule as `isTextualCandidate`. */
284
+ export function isBlindSpotCandidate(decl) {
285
+ return decl?.injected !== true;
286
+ }
287
+ /** The checker may miss uses: JS declaring file, a string-registered name, or no tsconfig at all. */
270
288
  export function isUntyped(opened, decl, file) {
271
- if (decl?.parentName !== undefined && /^(states|routes)\./.test(decl.parentName) && !decl.parentName.endsWith(".resolve"))
289
+ if (!isTextualCandidate(decl))
272
290
  return false;
273
291
  return decl?.nameless === true || opened.index.project.implicit || (file !== undefined && /\.[mc]?jsx?$/.test(file));
274
292
  }
@@ -280,16 +298,22 @@ export function isPlainLocal(decl) {
280
298
  * Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded.
281
299
  * With `onlyFile` (absolute), that file alone.
282
300
  */
283
- export function textualCount(opened, name, changeSet = new Map(), onlyFile) {
301
+ export function textualCount(opened, needle, changeSet = new Map(), onlyFile) {
302
+ const { text: name, token } = needle;
284
303
  let matches = 0;
285
304
  let files = 0;
286
- for (const abs of onlyFile !== undefined ? [onlyFile] : opened.index.files) {
305
+ const project = new Set(opened.index.files);
306
+ // State names are also written in templates (`ui-sref`), which the Project does not list.
307
+ const candidates = onlyFile !== undefined ? [onlyFile] : [...project];
308
+ if (token && onlyFile === undefined)
309
+ walkExtra(opened.root, candidates);
310
+ for (const abs of new Set(candidates)) {
287
311
  const text = opened.index.text(abs);
288
312
  if (text === undefined || !text.includes(name))
289
313
  continue;
290
314
  const rel = opened.index.rel(abs);
291
- const declared = new Set(opened.index.declarations(abs).filter((d) => d.name === name).map((d) => d.nameLine));
292
- const n = wordMatches(text, name).filter((m) => !declared.has(m.line) && !inRanges(changeSet.get(rel), m.line)).length;
315
+ const declared = new Set(project.has(abs) ? opened.index.declarations(abs).filter((d) => d.name === name).map((d) => d.nameLine) : []);
316
+ const n = wordMatches(text, name, token).filter((m) => !declared.has(m.line) && !inRanges(changeSet.get(rel), m.line)).length;
293
317
  matches += n;
294
318
  if (n > 0)
295
319
  files++;
@@ -542,9 +566,12 @@ export async function impactCommand(args, ctx) {
542
566
  if (!row.live || !isUntyped(opened, row.live, row.live.file))
543
567
  continue;
544
568
  const localTo = isPlainLocal(row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
545
- row.textual = textualCount(opened, row.live.name, changeSet, localTo?.get(row.live.name));
569
+ const needle = textualNeedle(row.live);
570
+ if (!needle)
571
+ continue;
572
+ row.textual = textualCount(opened, needle, changeSet, localTo?.get(row.live.name));
546
573
  if (row.textual.matches > 0)
547
- row.unresolved = textualMatches(opened, semantic, [row.live.name], known, new Set(), changeSet, localTo).length;
574
+ row.unresolved = textualMatches(opened, semantic, [needle], known, new Set(), changeSet, localTo).length;
548
575
  }
549
576
  // Template usages: directives, components, bindings, controllers and filters named in .html and friends.
550
577
  let templateFiles;
@@ -598,14 +625,20 @@ export async function impactCommand(args, ctx) {
598
625
  };
599
626
  // --- Blind spots, textual matches, deeper callers ---
600
627
  const names = new Map();
628
+ const textNames = new Map();
601
629
  // A name is file-scoped only when every changed Declaration carrying it is a plain local of one file.
602
630
  const localTo = new Map();
603
631
  const wide = new Set();
604
632
  for (const r of rows) {
605
633
  const decl = r.change.after ?? r.change.before;
606
634
  const name = decl.name;
607
- if (!names.has(name))
635
+ if (isBlindSpotCandidate(decl) && !names.has(name))
608
636
  names.set(name, r.symbol);
637
+ const needle = isTextualCandidate(decl) ? textualNeedle(decl) : undefined;
638
+ if (!needle)
639
+ continue;
640
+ if (!textNames.has(needle.text))
641
+ textNames.set(needle.text, needle);
609
642
  const abs = opened.index.abs(decl.file);
610
643
  if (!isPlainLocal(decl) || (localTo.has(name) && localTo.get(name) !== abs))
611
644
  wide.add(name);
@@ -616,7 +649,7 @@ export async function impactCommand(args, ctx) {
616
649
  localTo.delete(name);
617
650
  const spots = blindSpots(semantic, names, changeSet);
618
651
  const textRows = grep
619
- ? textualMatches(opened, semantic, [...names.keys()], known, new Set(spots.map((s) => `${s.file}:${s.line}`)), changeSet, localTo)
652
+ ? textualMatches(opened, semantic, [...textNames.values()], known, new Set(spots.map((s) => `${s.file}:${s.line}`)), changeSet, localTo)
620
653
  : [];
621
654
  const unresolved = grep ? withTemplateRows(textRows, rows, templateFiles ?? []) : [];
622
655
  const touched = [...new Set([...analysed.filter((f) => f.status !== "deleted").map((f) => f.path), ...listed.flatMap((r) => r.outside.map((e) => e.file))])];
@@ -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, isUntyped, textualCount, textualHint, textualLine, textualMatches } from "./impact.js";
4
+ import { isPlainLocal, isTextualCandidate, isUntyped, textualCount, textualNeedle, textualHint, textualLine, textualMatches } 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";
@@ -23,7 +23,7 @@ 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); 'n matches in this file' for a non-exported local; above refs_total = uses the checker cannot resolve - see refs --grep
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
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`;
28
28
  export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
29
29
  flags[12]:
@@ -151,10 +151,11 @@ async function run(args, ctx, invoked) {
151
151
  const explicit = Boolean(project || opened.settings.project);
152
152
  const declaredFile = decl?.file ?? (def && def.line > 0 ? def.file : undefined);
153
153
  const localFile = isPlainLocal(decl) ? opened.index.abs(decl.file) : undefined;
154
- const textual = declaredFile !== undefined && isUntyped(opened, decl, declaredFile) ? textualCount(opened, symbol.split(".").pop(), new Map(), localFile) : undefined;
154
+ const needle = textualNeedle(decl ?? symbol, kind);
155
+ const textual = declaredFile !== undefined && needle && isUntyped(opened, decl, declaredFile) ? textualCount(opened, needle, new Map(), localFile) : undefined;
155
156
  const tplTarget = decl ? templateTarget(decl, opened.index.declarations(opened.index.abs(decl.file))) : undefined;
156
157
  const templates = tplTarget ? templateRows(loadTemplates(opened.root), tplTarget) : undefined;
157
- const o = { started, symbol, declaredAt, show, scope, explicit, label, name, absFile: resolved.absFile, ...(decl ? { decl } : {}), ...(textual ? { textual } : {}), ...(templates ? { templates } : {}) };
158
+ const o = { started, symbol, kind, declaredAt, show, scope, explicit, label, name, absFile: resolved.absFile, ...(decl ? { decl } : {}), ...(textual ? { textual } : {}), ...(templates ? { templates } : {}) };
158
159
  return mode === "calls"
159
160
  ? renderCallers(semantic, opened, resolved, header, { ...o, limit, depth })
160
161
  : renderRefs(semantic, opened, resolved, header, { ...o, limit, kind });
@@ -197,7 +198,9 @@ function outsideImporters(opened, o) {
197
198
  const inProject = new Set(opened.index.files);
198
199
  const barrels = new Set(closure.barrels);
199
200
  const targets = [o.absFile, ...closure.barrels];
200
- const needle = o.symbol.split(".").pop();
201
+ const needle = textualNeedle(o.symbol, o.kind)?.text;
202
+ if (!needle)
203
+ return { fields: {}, help: [] };
201
204
  const rows = closure.importers
202
205
  .filter((f) => !inProject.has(f) && !barrels.has(f) && (opened.index.text(f) ?? "").includes(needle))
203
206
  .map((f) => {
@@ -242,7 +245,10 @@ function viaFields(via, bases) {
242
245
  }
243
246
  /** Whole-word matches of the name on lines that hold no known position (what `--grep` lists). */
244
247
  function unresolvedOf(opened, semantic, o, known) {
245
- return textualMatches(opened, semantic, [o.symbol.split(".").pop()], 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());
248
+ const needle = isTextualCandidate(o.decl) ? textualNeedle(o.decl ?? o.symbol, o.kind) : undefined;
249
+ if (!needle)
250
+ return [];
251
+ 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());
246
252
  }
247
253
  /**
248
254
  * `textual` field and, when the name occurs more often than refs_total and some matches sit on no
@@ -282,7 +288,7 @@ function renderRefs(semantic, opened, resolved, header, o) {
282
288
  help.push(`Run \`ts-axi refs ${o.declaredAt} --implementations\` to list the ${impls.length} implementations`);
283
289
  const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
284
290
  help.unshift(...outside.help);
285
- const word = o.symbol.split(".").pop();
291
+ const word = textualNeedle(o.symbol, o.kind)?.text ?? "";
286
292
  const unresolved = o.show.grep || o.textual || o.templates ? unresolvedOf(opened, semantic, o, [...rows, ...via, ...impls]) : [];
287
293
  const grepRows = withTemplates(unresolved, o.templates);
288
294
  // The --grep hint counts what --grep lists: textual and template rows together.
@@ -290,7 +296,7 @@ function renderRefs(semantic, opened, resolved, header, o) {
290
296
  const tpl = templateFields(o, o.show.grep || text.help.length > 0);
291
297
  if (text.help.length > 0)
292
298
  help.unshift(...text.help);
293
- else if (o.decl?.nameless && !o.show.grep && tpl.help.length === 0)
299
+ else if (o.decl?.nameless && word && !o.show.grep && tpl.help.length === 0)
294
300
  help.unshift(`Run \`ts-axi refs ${o.declaredAt} --grep\` for textual matches of '${word}'`);
295
301
  help.unshift(...tpl.help);
296
302
  if (isFunctionLike(o.kind))
@@ -222,10 +222,14 @@ export function localOnlyLine(entries, max = 10) {
222
222
  const rest = entries.length - shown.length;
223
223
  return `${entries.length} (${[...shown, ...(rest > 0 ? [`+${rest}`] : [])].join("; ")})`;
224
224
  }
225
- /** Whole-word occurrences of `name` in `text`: 1-based line and column. */
226
- export function wordMatches(text, name) {
227
- const escaped = name.replace(/[$]/g, "\\$");
228
- const re = new RegExp(`(?<![\\w$])${escaped}(?![\\w$])`, "g");
225
+ /**
226
+ * Whole-word occurrences of `name` in `text`: 1-based line and column. With `token`, `name` is a dotted
227
+ * path (a state name): escaped literally, and neither `.` nor an identifier character may touch it.
228
+ */
229
+ export function wordMatches(text, name, token = false) {
230
+ const re = token
231
+ ? new RegExp(`(?<![\\w$.])${name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}(?![\\w$.])`, "g")
232
+ : new RegExp(`(?<![\\w$])${name.replace(/[$]/g, "\\$")}(?![\\w$])`, "g");
229
233
  const result = [];
230
234
  const lines = text.split("\n");
231
235
  lines.forEach((lineText, i) => {
@@ -450,13 +450,13 @@ export function collectDeclarations(sf, file) {
450
450
  if (!name || !prop.name)
451
451
  continue;
452
452
  if (ts.isMethodDeclaration(prop)) {
453
- nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported: true, signature: fullSignatureOf(prop, sf), namePos: prop.name.getStart(sf) });
453
+ nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported: true, signature: fullSignatureOf(prop, sf), namePos: prop.name.getStart(sf), injected: true });
454
454
  }
455
455
  else if (ts.isPropertyAssignment(prop)) {
456
456
  const value = unwrap(prop.initializer);
457
457
  const fn = registeredFunction(value);
458
458
  if (fn)
459
- nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported: true, signature: `${name}${paramsText(fn, sf)}`, namePos: prop.name.getStart(sf) });
459
+ nested({ start: prop.getStart(sf), end: prop.end, kind: "method", name, parentName: holder, exported: true, signature: `${name}${paramsText(fn, sf)}`, namePos: prop.name.getStart(sf), injected: true });
460
460
  else if (ts.isObjectLiteralExpression(value) && depth < 3)
461
461
  stateMembers(value, `${holder}.${name}`, depth + 1);
462
462
  }
@@ -808,6 +808,7 @@ export function collectDeclarations(sf, file) {
808
808
  hasBody: r.hasBody,
809
809
  fullSignature: full,
810
810
  ...(r.nameless ? { nameless: true } : {}),
811
+ ...(r.injected ? { injected: true } : {}),
811
812
  };
812
813
  });
813
814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-axi",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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": [