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.
- package/dist/src/commands/impact.js +47 -14
- package/dist/src/commands/refs.js +14 -8
- package/dist/src/impact.js +8 -4
- package/dist/src/symbols.js +3 -2
- package/package.json +1 -1
|
@@ -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
|
|
268
|
-
*
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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, [
|
|
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, [...
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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))
|
package/dist/src/impact.js
CHANGED
|
@@ -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
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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) => {
|
package/dist/src/symbols.js
CHANGED
|
@@ -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.
|
|
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": [
|