ts-axi 0.1.6 → 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/README.md +1 -1
- package/dist/src/commands/impact.js +78 -21
- package/dist/src/commands/refs.js +15 -8
- package/dist/src/impact.js +8 -4
- package/dist/src/symbols.js +90 -20
- package/dist/src/templates.js +43 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ decls[4]{line,kind,name,exported,signature}:
|
|
|
175
175
|
17,function,cartBadge,true,directive cartBadge()
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
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. 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`.
|
|
178
|
+
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`.
|
|
179
179
|
|
|
180
180
|
## Monorepos
|
|
181
181
|
|
|
@@ -29,7 +29,7 @@ flags[7]:
|
|
|
29
29
|
output:
|
|
30
30
|
base, head, changes (count), changed[n]{symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}
|
|
31
31
|
change = signature | removed | type | added | body | modified (before unavailable) - riskiest first
|
|
32
|
-
textual = column added only when an untyped (JavaScript; implicit project) Symbol has more whole-word matches outside the change set than refs_outside - those are uses the checker cannot resolve; - for typed rows; such a Change is never local_only
|
|
32
|
+
textual = column added only when an untyped (JavaScript; implicit project) Symbol has more whole-word matches outside the change set than refs_outside - those are uses the checker cannot resolve (declaring file only for a non-exported local); - for typed rows; such a Change is never local_only
|
|
33
33
|
refs_* / files / tests count direct References only; via = References outside the change set that reach the Symbol through an interface or base member it implements (or name an override of it) - they MAY call it
|
|
34
34
|
refs[n]{symbol,file,line,context} = References outside the change set; truncated + refs_shown + refs_outside_total when cut
|
|
35
35
|
notes[n]{symbol,note} = a signature change of a member that implements an interface or base member (the contract may break)
|
|
@@ -231,22 +231,27 @@ function blindSpots(semantic, names, changeSet) {
|
|
|
231
231
|
function walkExtra(dir, outFiles) {
|
|
232
232
|
outFiles.push(...walkFiles(dir, { accept: (name) => GREP_EXTRA.some((ext) => name.endsWith(ext)) }).files);
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Whole-word matches of `names` in Project files and templates that are not `known` References;
|
|
236
|
+
* a name in `localTo` (name → absolute file) is searched in that file only.
|
|
237
|
+
*/
|
|
238
|
+
export function textualMatches(opened, semantic, names, known, skipLines, changeSet, localTo = new Map()) {
|
|
236
239
|
const files = [...semantic.projectFiles];
|
|
237
240
|
const projectSet = new Set(files);
|
|
238
241
|
walkExtra(opened.root, files);
|
|
239
242
|
const rows = [];
|
|
240
243
|
for (const abs of files) {
|
|
241
244
|
const text = opened.index.text(abs);
|
|
242
|
-
if (text === undefined || !names.some((n) => text.includes(n)))
|
|
245
|
+
if (text === undefined || !names.some((n) => text.includes(n.text)))
|
|
243
246
|
continue;
|
|
244
247
|
const rel = opened.index.rel(abs);
|
|
245
248
|
// A Declaration of another Symbol with the same name is provably not a use.
|
|
246
249
|
const declared = new Set(projectSet.has(abs) ? opened.index.declarations(abs).map((d) => d.nameLine) : []);
|
|
247
250
|
const lines = new Set();
|
|
248
|
-
for (const name of names) {
|
|
249
|
-
|
|
251
|
+
for (const { text: name, token } of names) {
|
|
252
|
+
if (localTo.has(name) && localTo.get(name) !== abs)
|
|
253
|
+
continue;
|
|
254
|
+
for (const m of wordMatches(text, name, token)) {
|
|
250
255
|
if (known.has(`${rel}:${m.line}`) || skipLines.has(`${rel}:${m.line}`) || declared.has(m.line) || inRanges(changeSet.get(rel), m.line))
|
|
251
256
|
continue;
|
|
252
257
|
if (lines.has(m.line))
|
|
@@ -258,34 +263,65 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
|
|
|
258
263
|
}
|
|
259
264
|
return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
260
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
|
+
}
|
|
261
270
|
/**
|
|
262
|
-
* The
|
|
263
|
-
*
|
|
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.
|
|
264
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. */
|
|
265
288
|
export function isUntyped(opened, decl, file) {
|
|
266
|
-
if (
|
|
289
|
+
if (!isTextualCandidate(decl))
|
|
267
290
|
return false;
|
|
268
291
|
return decl?.nameless === true || opened.index.project.implicit || (file !== undefined && /\.[mc]?jsx?$/.test(file));
|
|
269
292
|
}
|
|
270
|
-
/**
|
|
271
|
-
export function
|
|
293
|
+
/** A non-exported, non-member, identifier-named Declaration: only its own file can name it. */
|
|
294
|
+
export function isPlainLocal(decl) {
|
|
295
|
+
return decl !== undefined && !decl.exported && !decl.nameless && decl.parentName === undefined;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded.
|
|
299
|
+
* With `onlyFile` (absolute), that file alone.
|
|
300
|
+
*/
|
|
301
|
+
export function textualCount(opened, needle, changeSet = new Map(), onlyFile) {
|
|
302
|
+
const { text: name, token } = needle;
|
|
272
303
|
let matches = 0;
|
|
273
304
|
let files = 0;
|
|
274
|
-
|
|
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)) {
|
|
275
311
|
const text = opened.index.text(abs);
|
|
276
312
|
if (text === undefined || !text.includes(name))
|
|
277
313
|
continue;
|
|
278
314
|
const rel = opened.index.rel(abs);
|
|
279
|
-
const declared = new Set(opened.index.declarations(abs).filter((d) => d.name === name).map((d) => d.nameLine));
|
|
280
|
-
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;
|
|
281
317
|
matches += n;
|
|
282
318
|
if (n > 0)
|
|
283
319
|
files++;
|
|
284
320
|
}
|
|
285
|
-
return { matches, files };
|
|
321
|
+
return { matches, files, ...(onlyFile !== undefined ? { fileScoped: true } : {}) };
|
|
286
322
|
}
|
|
287
323
|
export function textualLine(count) {
|
|
288
|
-
return `${count.matches} matches in ${count.files} files`;
|
|
324
|
+
return count.fileScoped ? `${count.matches} matches in this file` : `${count.matches} matches in ${count.files} files`;
|
|
289
325
|
}
|
|
290
326
|
export function textualHint(command, gap) {
|
|
291
327
|
return `Run \`${command}\` for the ${gap} textual matches the checker cannot resolve (untyped JavaScript)`;
|
|
@@ -529,9 +565,13 @@ export async function impactCommand(args, ctx) {
|
|
|
529
565
|
for (const row of rows) {
|
|
530
566
|
if (!row.live || !isUntyped(opened, row.live, row.live.file))
|
|
531
567
|
continue;
|
|
532
|
-
row.
|
|
568
|
+
const localTo = isPlainLocal(row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
|
|
569
|
+
const needle = textualNeedle(row.live);
|
|
570
|
+
if (!needle)
|
|
571
|
+
continue;
|
|
572
|
+
row.textual = textualCount(opened, needle, changeSet, localTo?.get(row.live.name));
|
|
533
573
|
if (row.textual.matches > 0)
|
|
534
|
-
row.unresolved = textualMatches(opened, semantic, [
|
|
574
|
+
row.unresolved = textualMatches(opened, semantic, [needle], known, new Set(), changeSet, localTo).length;
|
|
535
575
|
}
|
|
536
576
|
// Template usages: directives, components, bindings, controllers and filters named in .html and friends.
|
|
537
577
|
let templateFiles;
|
|
@@ -585,14 +625,31 @@ export async function impactCommand(args, ctx) {
|
|
|
585
625
|
};
|
|
586
626
|
// --- Blind spots, textual matches, deeper callers ---
|
|
587
627
|
const names = new Map();
|
|
628
|
+
const textNames = new Map();
|
|
629
|
+
// A name is file-scoped only when every changed Declaration carrying it is a plain local of one file.
|
|
630
|
+
const localTo = new Map();
|
|
631
|
+
const wide = new Set();
|
|
588
632
|
for (const r of rows) {
|
|
589
|
-
const
|
|
590
|
-
|
|
633
|
+
const decl = r.change.after ?? r.change.before;
|
|
634
|
+
const name = decl.name;
|
|
635
|
+
if (isBlindSpotCandidate(decl) && !names.has(name))
|
|
591
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);
|
|
642
|
+
const abs = opened.index.abs(decl.file);
|
|
643
|
+
if (!isPlainLocal(decl) || (localTo.has(name) && localTo.get(name) !== abs))
|
|
644
|
+
wide.add(name);
|
|
645
|
+
else
|
|
646
|
+
localTo.set(name, abs);
|
|
592
647
|
}
|
|
648
|
+
for (const name of wide)
|
|
649
|
+
localTo.delete(name);
|
|
593
650
|
const spots = blindSpots(semantic, names, changeSet);
|
|
594
651
|
const textRows = grep
|
|
595
|
-
? textualMatches(opened, semantic, [...
|
|
652
|
+
? textualMatches(opened, semantic, [...textNames.values()], known, new Set(spots.map((s) => `${s.file}:${s.line}`)), changeSet, localTo)
|
|
596
653
|
: [];
|
|
597
654
|
const unresolved = grep ? withTemplateRows(textRows, rows, templateFiles ?? []) : [];
|
|
598
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 { 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); 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]:
|
|
@@ -150,10 +150,12 @@ async function run(args, ctx, invoked) {
|
|
|
150
150
|
const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
|
|
151
151
|
const explicit = Boolean(project || opened.settings.project);
|
|
152
152
|
const declaredFile = decl?.file ?? (def && def.line > 0 ? def.file : undefined);
|
|
153
|
-
const
|
|
153
|
+
const localFile = isPlainLocal(decl) ? opened.index.abs(decl.file) : 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;
|
|
154
156
|
const tplTarget = decl ? templateTarget(decl, opened.index.declarations(opened.index.abs(decl.file))) : undefined;
|
|
155
157
|
const templates = tplTarget ? templateRows(loadTemplates(opened.root), tplTarget) : undefined;
|
|
156
|
-
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 } : {}) };
|
|
157
159
|
return mode === "calls"
|
|
158
160
|
? renderCallers(semantic, opened, resolved, header, { ...o, limit, depth })
|
|
159
161
|
: renderRefs(semantic, opened, resolved, header, { ...o, limit, kind });
|
|
@@ -196,7 +198,9 @@ function outsideImporters(opened, o) {
|
|
|
196
198
|
const inProject = new Set(opened.index.files);
|
|
197
199
|
const barrels = new Set(closure.barrels);
|
|
198
200
|
const targets = [o.absFile, ...closure.barrels];
|
|
199
|
-
const needle = o.symbol.
|
|
201
|
+
const needle = textualNeedle(o.symbol, o.kind)?.text;
|
|
202
|
+
if (!needle)
|
|
203
|
+
return { fields: {}, help: [] };
|
|
200
204
|
const rows = closure.importers
|
|
201
205
|
.filter((f) => !inProject.has(f) && !barrels.has(f) && (opened.index.text(f) ?? "").includes(needle))
|
|
202
206
|
.map((f) => {
|
|
@@ -241,7 +245,10 @@ function viaFields(via, bases) {
|
|
|
241
245
|
}
|
|
242
246
|
/** Whole-word matches of the name on lines that hold no known position (what `--grep` lists). */
|
|
243
247
|
function unresolvedOf(opened, semantic, o, known) {
|
|
244
|
-
|
|
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());
|
|
245
252
|
}
|
|
246
253
|
/**
|
|
247
254
|
* `textual` field and, when the name occurs more often than refs_total and some matches sit on no
|
|
@@ -281,7 +288,7 @@ function renderRefs(semantic, opened, resolved, header, o) {
|
|
|
281
288
|
help.push(`Run \`ts-axi refs ${o.declaredAt} --implementations\` to list the ${impls.length} implementations`);
|
|
282
289
|
const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
|
|
283
290
|
help.unshift(...outside.help);
|
|
284
|
-
const word = o.symbol.
|
|
291
|
+
const word = textualNeedle(o.symbol, o.kind)?.text ?? "";
|
|
285
292
|
const unresolved = o.show.grep || o.textual || o.templates ? unresolvedOf(opened, semantic, o, [...rows, ...via, ...impls]) : [];
|
|
286
293
|
const grepRows = withTemplates(unresolved, o.templates);
|
|
287
294
|
// The --grep hint counts what --grep lists: textual and template rows together.
|
|
@@ -289,7 +296,7 @@ function renderRefs(semantic, opened, resolved, header, o) {
|
|
|
289
296
|
const tpl = templateFields(o, o.show.grep || text.help.length > 0);
|
|
290
297
|
if (text.help.length > 0)
|
|
291
298
|
help.unshift(...text.help);
|
|
292
|
-
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)
|
|
293
300
|
help.unshift(`Run \`ts-axi refs ${o.declaredAt} --grep\` for textual matches of '${word}'`);
|
|
294
301
|
help.unshift(...tpl.help);
|
|
295
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
|
@@ -232,6 +232,8 @@ const CONSTRUCTOR_VERBS = new Set(["controller", "service", "provider"]);
|
|
|
232
232
|
/** Router providers whose `.state(...)` / `.when(...)` calls declare a state (ui-router, ngRoute). */
|
|
233
233
|
const STATE_PROVIDER = /^\$?stateProvider$/i;
|
|
234
234
|
const ROUTE_PROVIDER = /^\$?routeProvider$/i;
|
|
235
|
+
/** Keys that, next to a string `name`, make an array element a ui-router state object. */
|
|
236
|
+
const STATE_KEYS = new Set(["url", "resolve", "component", "controller", "template", "templateUrl", "views", "abstract"]);
|
|
235
237
|
/** Directive definition keys whose object literal lists the bindings. */
|
|
236
238
|
const BINDING_KEYS = new Set(["scope", "bindToController", "bindings"]);
|
|
237
239
|
/** `Template.x.<hook>(fn)` lifecycle callbacks. */
|
|
@@ -448,52 +450,111 @@ export function collectDeclarations(sf, file) {
|
|
|
448
450
|
if (!name || !prop.name)
|
|
449
451
|
continue;
|
|
450
452
|
if (ts.isMethodDeclaration(prop)) {
|
|
451
|
-
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 });
|
|
452
454
|
}
|
|
453
455
|
else if (ts.isPropertyAssignment(prop)) {
|
|
454
456
|
const value = unwrap(prop.initializer);
|
|
455
457
|
const fn = registeredFunction(value);
|
|
456
458
|
if (fn)
|
|
457
|
-
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 });
|
|
458
460
|
else if (ts.isObjectLiteralExpression(value) && depth < 3)
|
|
459
461
|
stateMembers(value, `${holder}.${name}`, depth + 1);
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
};
|
|
463
|
-
/**
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
465
|
+
/** Same-file string consts (`const base = 'main.x'`), for state names built from them; ambiguous names dropped. */
|
|
466
|
+
let stringConsts;
|
|
467
|
+
const constValue = (id, depth) => {
|
|
468
|
+
if (!stringConsts) {
|
|
469
|
+
stringConsts = new Map();
|
|
470
|
+
const visit = (node) => {
|
|
471
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
472
|
+
const init = unwrap(node.initializer);
|
|
473
|
+
if (isStringName(init) || ts.isTemplateExpression(init))
|
|
474
|
+
stringConsts.set(node.name.text, stringConsts.has(node.name.text) ? null : init);
|
|
475
|
+
}
|
|
476
|
+
ts.forEachChild(node, visit);
|
|
477
|
+
};
|
|
478
|
+
visit(sf);
|
|
479
|
+
}
|
|
480
|
+
const init = stringConsts.get(id);
|
|
481
|
+
return init && depth < 3 ? stateNameOf(init, depth + 1) : undefined;
|
|
482
|
+
};
|
|
483
|
+
/** A literal, a template literal (`${...}` = a same-file string const, else `*`) or a const identifier. */
|
|
484
|
+
const stateNameOf = (expr, depth = 0) => {
|
|
485
|
+
const e = unwrap(expr);
|
|
486
|
+
if (isStringName(e))
|
|
487
|
+
return e.text;
|
|
488
|
+
if (ts.isTemplateExpression(e)) {
|
|
489
|
+
return e.head.text + e.templateSpans.map((s) => (ts.isIdentifier(s.expression) ? (constValue(s.expression.text, depth) ?? "*") : "*") + s.literal.text).join("");
|
|
490
|
+
}
|
|
491
|
+
if (ts.isIdentifier(e))
|
|
492
|
+
return constValue(e.text, depth) ?? e.text;
|
|
493
|
+
return undefined;
|
|
494
|
+
};
|
|
495
|
+
const propOf = (obj, key) => obj.properties.find((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && nameText(p.name, sf) === key);
|
|
496
|
+
/** `name` of a state object literal: `name: 'x'`, `` name: `${base}.x` ``, `name: base` or `{ name }`. */
|
|
497
|
+
const stateObjectName = (obj) => {
|
|
498
|
+
const prop = propOf(obj, "name");
|
|
499
|
+
if (prop && ts.isPropertyAssignment(prop))
|
|
500
|
+
return stateNameOf(prop.initializer);
|
|
501
|
+
if (prop && ts.isShorthandPropertyAssignment(prop))
|
|
502
|
+
return stateNameOf(prop.name);
|
|
503
|
+
return undefined;
|
|
504
|
+
};
|
|
505
|
+
/** A ui-router state object (`.state()` argument or array element) or an ngRoute config as a `state` Declaration plus its callbacks. */
|
|
506
|
+
const stateFromObject = (obj, name, start, end, route) => {
|
|
507
|
+
const url = propOf(obj, "url");
|
|
508
|
+
const urlText = url && ts.isPropertyAssignment(url) && isStringName(url.initializer) ? url.initializer.text : undefined;
|
|
477
509
|
const parent = route ? "routes" : "states";
|
|
478
510
|
// Plain-literal options are compared, not displayed: a new controller or template is a contract change.
|
|
479
511
|
const options = obj.properties
|
|
480
512
|
.filter((p) => ts.isPropertyAssignment(p) && nameText(p.name, sf) !== "name" && nameText(p.name, sf) !== "url")
|
|
481
513
|
.filter((p) => isStringName(p.initializer) || p.initializer.kind === ts.SyntaxKind.TrueKeyword || p.initializer.kind === ts.SyntaxKind.FalseKeyword)
|
|
482
514
|
.map((p) => tokenText(p, sf, p.getStart(sf), p.end));
|
|
483
|
-
const callee = call.expression;
|
|
484
515
|
nested({
|
|
485
|
-
start
|
|
486
|
-
end
|
|
516
|
+
start,
|
|
517
|
+
end,
|
|
487
518
|
kind: "state",
|
|
488
519
|
name,
|
|
489
520
|
parentName: parent,
|
|
490
521
|
exported: true,
|
|
491
|
-
signature: `${route ? "route" : "state"} ${name}${
|
|
522
|
+
signature: `${route ? "route" : "state"} ${name}${urlText !== undefined && !route ? ` (url ${urlText})` : ""}${options.length > 0 ? `\n${options.join("; ")}` : ""}`,
|
|
492
523
|
namePos: obj.getStart(sf),
|
|
493
524
|
nameless: true,
|
|
494
525
|
});
|
|
495
526
|
stateMembers(obj, `${parent}.${name}`, 1);
|
|
496
527
|
};
|
|
528
|
+
/** One `.state('name', {...})`, `.state({ name, ... })` or `.when('/path', {...})` call. */
|
|
529
|
+
const stateCall = (call, route) => {
|
|
530
|
+
const [first, second] = call.arguments;
|
|
531
|
+
const obj = first && ts.isObjectLiteralExpression(first) && !route ? first : second && ts.isObjectLiteralExpression(second) ? second : undefined;
|
|
532
|
+
if (!obj)
|
|
533
|
+
return;
|
|
534
|
+
const name = isStringName(first) ? first.text : route ? undefined : stateObjectName(obj);
|
|
535
|
+
if (name === undefined)
|
|
536
|
+
return;
|
|
537
|
+
stateFromObject(obj, name, call.expression.name.getStart(sf), call.end, route);
|
|
538
|
+
};
|
|
539
|
+
/** State objects among the elements of an array literal, or of `[...].concat([...], ...)`; other values ignored. */
|
|
540
|
+
const stateArray = (expr) => {
|
|
541
|
+
const e = unwrap(expr);
|
|
542
|
+
if (ts.isArrayLiteralExpression(e)) {
|
|
543
|
+
for (const el of e.elements) {
|
|
544
|
+
const obj = unwrap(el);
|
|
545
|
+
if (!ts.isObjectLiteralExpression(obj) || !obj.properties.some((p) => STATE_KEYS.has(nameText(p.name, sf) ?? "")))
|
|
546
|
+
continue;
|
|
547
|
+
const name = stateObjectName(obj);
|
|
548
|
+
if (name !== undefined)
|
|
549
|
+
stateFromObject(obj, name, obj.getStart(sf), obj.end, false);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "concat") {
|
|
553
|
+
stateArray(e.expression.expression);
|
|
554
|
+
for (const arg of e.arguments)
|
|
555
|
+
stateArray(arg);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
497
558
|
/** `$stateProvider.state(...).state(...)` and `$routeProvider.when(...)` chains; `.otherwise()` ignored. */
|
|
498
559
|
const routerChain = (expr) => {
|
|
499
560
|
const calls = [];
|
|
@@ -567,8 +628,12 @@ export function collectDeclarations(sf, file) {
|
|
|
567
628
|
if (target === "module.exports") {
|
|
568
629
|
if (ts.isObjectLiteralExpression(right))
|
|
569
630
|
objectMembers(right, "exports", true);
|
|
631
|
+
else
|
|
632
|
+
stateArray(right);
|
|
570
633
|
return;
|
|
571
634
|
}
|
|
635
|
+
if (target === `exports.${left.name.text}` || target === `module.exports.${left.name.text}`)
|
|
636
|
+
stateArray(right);
|
|
572
637
|
const owner = left.expression;
|
|
573
638
|
if (ts.isPropertyAccessExpression(owner) && owner.name.text === "prototype" && ts.isIdentifier(owner.expression)) {
|
|
574
639
|
const cls = owner.expression.text;
|
|
@@ -614,6 +679,7 @@ export function collectDeclarations(sf, file) {
|
|
|
614
679
|
routerBody(fn.body);
|
|
615
680
|
const exported = inIife ? false : topLevelExported(name);
|
|
616
681
|
const init = unwrap(decl.initializer);
|
|
682
|
+
stateArray(init);
|
|
617
683
|
if (ts.isObjectLiteralExpression(init))
|
|
618
684
|
objectMembers(init, name, exported);
|
|
619
685
|
else if (fn && isConstructorName(name))
|
|
@@ -637,6 +703,9 @@ export function collectDeclarations(sf, file) {
|
|
|
637
703
|
else
|
|
638
704
|
callChain(expr);
|
|
639
705
|
}
|
|
706
|
+
else if (ts.isExportAssignment(statement)) {
|
|
707
|
+
stateArray(statement.expression);
|
|
708
|
+
}
|
|
640
709
|
else if (ts.isIfStatement(statement)) {
|
|
641
710
|
scriptStatement(statement.thenStatement, inIife);
|
|
642
711
|
if (statement.elseStatement)
|
|
@@ -739,6 +808,7 @@ export function collectDeclarations(sf, file) {
|
|
|
739
808
|
hasBody: r.hasBody,
|
|
740
809
|
fullSignature: full,
|
|
741
810
|
...(r.nameless ? { nameless: true } : {}),
|
|
811
|
+
...(r.injected ? { injected: true } : {}),
|
|
742
812
|
};
|
|
743
813
|
});
|
|
744
814
|
}
|
package/dist/src/templates.js
CHANGED
|
@@ -53,6 +53,46 @@ export function loadTemplates(root) {
|
|
|
53
53
|
}
|
|
54
54
|
return files;
|
|
55
55
|
}
|
|
56
|
+
const TAG_SPAN_LINES = 40;
|
|
57
|
+
/**
|
|
58
|
+
* Text of the tag open at `offset` (from its `<`), or undefined when `offset` is outside a tag or
|
|
59
|
+
* inside a quoted value. Forward scan from up to 40 lines back; `>` inside quotes does not close.
|
|
60
|
+
* Tolerant of broken markup: never throws, an unclosed quote just ends the tag at the scan point.
|
|
61
|
+
*/
|
|
62
|
+
function openTagBefore(text, offset, lineStarts) {
|
|
63
|
+
let lo = 0;
|
|
64
|
+
let hi = lineStarts.length - 1;
|
|
65
|
+
while (lo < hi) {
|
|
66
|
+
const mid = (lo + hi + 1) >> 1;
|
|
67
|
+
if (lineStarts[mid] <= offset)
|
|
68
|
+
lo = mid;
|
|
69
|
+
else
|
|
70
|
+
hi = mid - 1;
|
|
71
|
+
}
|
|
72
|
+
let tagStart = -1;
|
|
73
|
+
let quote = "";
|
|
74
|
+
for (let i = lineStarts[Math.max(0, lo - TAG_SPAN_LINES)]; i < offset; i++) {
|
|
75
|
+
const c = text[i];
|
|
76
|
+
if (tagStart < 0) {
|
|
77
|
+
if (c === "<" && /[A-Za-z]/.test(text[i + 1] ?? ""))
|
|
78
|
+
tagStart = i;
|
|
79
|
+
}
|
|
80
|
+
else if (quote) {
|
|
81
|
+
if (c === quote)
|
|
82
|
+
quote = "";
|
|
83
|
+
}
|
|
84
|
+
else if (c === '"' || c === "'") {
|
|
85
|
+
quote = c;
|
|
86
|
+
}
|
|
87
|
+
else if (c === ">") {
|
|
88
|
+
tagStart = -1;
|
|
89
|
+
}
|
|
90
|
+
else if (c === "<" && /[A-Za-z]/.test(text[i + 1] ?? "")) {
|
|
91
|
+
tagStart = i;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return tagStart < 0 || quote ? undefined : text.slice(tagStart, offset);
|
|
95
|
+
}
|
|
56
96
|
/** Usages of `target` in one template text: at most one per line. */
|
|
57
97
|
export function usagesIn(text, target) {
|
|
58
98
|
const result = [];
|
|
@@ -66,12 +106,11 @@ export function usagesIn(text, target) {
|
|
|
66
106
|
const offset = (m.index ?? 0) + (/^[<\s"'|]/.test(m[0]) ? 1 : 0);
|
|
67
107
|
if (target.owner) {
|
|
68
108
|
// The attribute counts only inside a tag opened by (or carrying) the owner directive.
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
109
|
+
const tag = openTagBefore(text, offset, lineStarts);
|
|
110
|
+
if (tag === undefined)
|
|
71
111
|
continue;
|
|
72
|
-
const tag = text.slice(tagStart, offset);
|
|
73
112
|
target.owner.lastIndex = 0;
|
|
74
|
-
if (
|
|
113
|
+
if (!target.owner.test(tag))
|
|
75
114
|
continue;
|
|
76
115
|
}
|
|
77
116
|
let lo = 0;
|
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": [
|