ts-axi 0.1.1 → 0.1.3
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 +21 -7
- package/dist/src/commands/common.js +10 -1
- package/dist/src/commands/def.js +2 -2
- package/dist/src/commands/home.js +35 -43
- package/dist/src/commands/impact.js +89 -38
- package/dist/src/commands/refs.js +165 -27
- package/dist/src/importers.js +182 -0
- package/dist/src/project.js +301 -69
- package/dist/src/references.js +69 -0
- package/dist/src/scope.js +119 -0
- package/dist/src/walk.js +96 -0
- package/dist/src/workspace.js +297 -0
- package/package.json +1 -1
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { relative, sep } from "node:path";
|
|
2
|
+
import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
|
|
3
|
+
import { importerClosure } from "../importers.js";
|
|
4
|
+
import { findOutsideProject, nearestTsconfig } from "../project.js";
|
|
5
|
+
import { viaLine } from "../references.js";
|
|
3
6
|
import { block, helpBlock, out } from "../render.js";
|
|
7
|
+
import { importerIndexFor, REFS_HOPS, scopeFields, scopeHelp, semanticFor } from "../scope.js";
|
|
8
|
+
import { byDepthThenPath } from "../walk.js";
|
|
9
|
+
import { detectWorkspace, isMultiProject } from "../workspace.js";
|
|
4
10
|
import { isTestFile } from "../settings.js";
|
|
5
11
|
import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
|
|
6
|
-
import { contextOf, declarationAt, filterSuffix, lineOf, matchRows, narrowHints, open, positionStartDir, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
|
|
12
|
+
import { contextOf, declarationAt, filterSuffix, lineOf, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
|
|
7
13
|
const DEFAULT_LIMIT = 50;
|
|
8
14
|
const MAX_LIMIT = 500;
|
|
9
15
|
const DEPTH_ROWS = 50;
|
|
10
|
-
const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), -P <tsconfig>, --json`;
|
|
16
|
+
const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), --via (adds via rows with a via column), -P <tsconfig>, --json`;
|
|
17
|
+
const VIA_DOC = `via_interface: n (Iface.member) = References through an interface or base member the Symbol implements or overrides - they MAY reach it; not in refs_total
|
|
18
|
+
via_implementations: n (Impl.member) = References to implementations or overrides of the Symbol; not in refs_total
|
|
19
|
+
implementations: n = members that implement or override the Symbol (their Declarations are never refs)
|
|
20
|
+
matches: 0 under -P lists found_outside_project[n]{file,line} when the name is declared outside that Project
|
|
21
|
+
refs_total: 0 under -P in a workspace lists found_outside_project[n]{file,line} = importers outside that Project
|
|
22
|
+
scope = in a monorepo without -P the program is the declaring file + its importers (scope_partial: true when the importer scan was capped)
|
|
23
|
+
tsconfig: x (n referenced configs) = a references-only tsconfig expanded to the configs it references`;
|
|
11
24
|
export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
|
|
12
|
-
flags[
|
|
25
|
+
flags[11]:
|
|
13
26
|
${SHARED_FLAGS}
|
|
27
|
+
--implementations (lists implementations[n]{file,line,symbol} instead of the count)
|
|
14
28
|
--kind calls = same as \`ts-axi callers\`
|
|
15
29
|
output:
|
|
16
30
|
symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,context} (Declarations excluded), elapsed_ms
|
|
31
|
+
${VIA_DOC}
|
|
17
32
|
an ambiguous name lists matches[n] + the disambiguated forms; nothing is analysed
|
|
18
33
|
examples[4]:
|
|
19
34
|
ts-axi refs User.save
|
|
@@ -22,12 +37,13 @@ examples[4]:
|
|
|
22
37
|
ts-axi refs runSync --limit 200
|
|
23
38
|
`;
|
|
24
39
|
export const CALLERS_HELP = `usage: ts-axi callers <name | Class.member | path:line[:col]> [flags]
|
|
25
|
-
flags[
|
|
40
|
+
flags[11]:
|
|
26
41
|
${SHARED_FLAGS}
|
|
27
42
|
--depth <1-3> (default 1; levels > 1 print callers_depthN[n]{caller,file,count}, no locations)
|
|
28
43
|
output:
|
|
29
44
|
symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,caller,context}, elapsed_ms
|
|
30
45
|
caller = enclosing function or method; (top-level) for module code
|
|
46
|
+
${VIA_DOC}
|
|
31
47
|
examples[3]:
|
|
32
48
|
ts-axi callers runSync
|
|
33
49
|
ts-axi callers User.save --depth 2
|
|
@@ -45,7 +61,8 @@ async function run(args, ctx, invoked) {
|
|
|
45
61
|
...TARGET_FLAGS,
|
|
46
62
|
{ name: "--limit" },
|
|
47
63
|
{ name: "--test-glob", repeatable: true },
|
|
48
|
-
|
|
64
|
+
{ name: "--via", boolean: true },
|
|
65
|
+
...(invoked === "calls" ? [{ name: "--depth" }] : [{ name: "--implementations", boolean: true }]),
|
|
49
66
|
];
|
|
50
67
|
const parsed = parseArgs(args, specs, command);
|
|
51
68
|
const [raw] = expectPositionals(parsed, 1, `${command} <name | Class.member | path:line[:col]> [flags]`);
|
|
@@ -57,6 +74,7 @@ async function run(args, ctx, invoked) {
|
|
|
57
74
|
const label = targetLabel(target);
|
|
58
75
|
const project = flag(parsed, "-P");
|
|
59
76
|
const name = mode === "calls" ? "callers" : "refs";
|
|
77
|
+
const show = { via: bool(parsed, "--via"), implementations: invoked === "refs" && bool(parsed, "--implementations") };
|
|
60
78
|
const started = performance.now();
|
|
61
79
|
const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
|
|
62
80
|
const warning = opened.warning ? { warning: opened.warning } : {};
|
|
@@ -65,6 +83,23 @@ async function run(args, ctx, invoked) {
|
|
|
65
83
|
if (target.kind === "name") {
|
|
66
84
|
const groups = resolveNameTarget(opened, target, f);
|
|
67
85
|
if (groups.length === 0) {
|
|
86
|
+
// Under an explicit Project the name may live in a sibling one: say so before suggesting --fuzzy.
|
|
87
|
+
const outside = project || opened.settings.project
|
|
88
|
+
? findOutsideProject(opened.index, { name: target.name, ...(target.member !== undefined ? { member: target.member } : {}), fuzzy: f.fuzzy })
|
|
89
|
+
: [];
|
|
90
|
+
if (outside.length > 0) {
|
|
91
|
+
const tsconfig = nearestTsconfig(opened.index.abs(outside[0].file), opened.root);
|
|
92
|
+
const rel = tsconfig ? relative(ctx.cwd, tsconfig).split(sep).join("/") : undefined;
|
|
93
|
+
return out(block({
|
|
94
|
+
...warning,
|
|
95
|
+
matches: 0,
|
|
96
|
+
found_outside_project: outside.map((d) => ({ file: d.file, line: d.nameLine })),
|
|
97
|
+
hint: `symbol is declared outside the selected project - run with -P ${rel ?? "<its tsconfig>"} or without -P`,
|
|
98
|
+
}), helpBlock([
|
|
99
|
+
...(rel ? [`Run \`ts-axi ${name} ${label} -P ${rel}\``] : []),
|
|
100
|
+
`Run \`ts-axi ${name} ${outside[0].file}:${outside[0].nameLine}\` to use the nearest tsconfig of that file`,
|
|
101
|
+
]));
|
|
102
|
+
}
|
|
68
103
|
const suggested = suggestions(opened, target);
|
|
69
104
|
return out(block({ ...warning, matches: 0, ...(suggested.length > 0 ? { did_you_mean: suggested } : {}) }), helpBlock([
|
|
70
105
|
...suggested.slice(0, 2).map((s) => `Run \`ts-axi ${name} ${s}\``),
|
|
@@ -75,7 +110,7 @@ async function run(args, ctx, invoked) {
|
|
|
75
110
|
return out(block({ ...warning, symbol: label, ambiguous: `${groups.length} Symbols - pick one`, matches: matchRows(groups) }), helpBlock(narrowHints(name, groups, f, `${label}${filterSuffix(f, project)}`)));
|
|
76
111
|
}
|
|
77
112
|
const decl = groups[0].decls[0];
|
|
78
|
-
resolved = { absFile: opened.index.abs(decl.file), pos: decl.namePos, decl };
|
|
113
|
+
resolved = { absFile: opened.index.abs(decl.file), pos: decl.namePos, decl, seeds: [...new Set(groups[0].decls.map((d) => opened.index.abs(d.file)))] };
|
|
79
114
|
}
|
|
80
115
|
else {
|
|
81
116
|
const at = resolvePosition(opened.index, ctx.cwd, target);
|
|
@@ -87,17 +122,62 @@ async function run(args, ctx, invoked) {
|
|
|
87
122
|
}
|
|
88
123
|
resolved = at.kind === "decl" ? { absFile: at.abs, pos: at.decl.namePos, decl: at.decl } : { absFile: at.abs, pos: at.pos };
|
|
89
124
|
}
|
|
90
|
-
|
|
125
|
+
let built = semanticFor(opened, resolved.seeds ?? [resolved.absFile], resolved.decl ? REFS_HOPS : 0);
|
|
126
|
+
if (!resolved.decl && built.scope) {
|
|
127
|
+
// A use position: find its Declaration in a small program, then scope around the declaring file.
|
|
128
|
+
const at = built.semantic.definitionAt(resolved.absFile, resolved.pos);
|
|
129
|
+
if (at && at.line > 0)
|
|
130
|
+
resolved = { absFile: at.absFile, pos: at.pos };
|
|
131
|
+
built = semanticFor(opened, [resolved.absFile], REFS_HOPS);
|
|
132
|
+
}
|
|
133
|
+
const { semantic, scope } = built;
|
|
91
134
|
const def = semantic.definitionAt(resolved.absFile, resolved.pos);
|
|
92
135
|
const decl = resolved.decl ??
|
|
93
136
|
(def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined);
|
|
94
137
|
const kind = decl?.kind ?? kindFromScriptElement(def?.kind ?? semantic.quickInfo(resolved.absFile, resolved.pos)?.kind ?? "");
|
|
95
138
|
const symbol = decl?.qualifiedName ?? def?.name ?? label;
|
|
96
139
|
const declaredAt = decl ? `${decl.file}:${decl.nameLine}` : def && def.line > 0 ? `${def.file}:${def.line}` : "(outside the Project)";
|
|
97
|
-
const header = { ...warning, symbol, kind, declared_at: declaredAt };
|
|
140
|
+
const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
|
|
141
|
+
const explicit = Boolean(project || opened.settings.project);
|
|
142
|
+
const o = { started, symbol, declaredAt, show, scope, explicit, label, name, absFile: resolved.absFile };
|
|
98
143
|
return mode === "calls"
|
|
99
|
-
? renderCallers(semantic, opened, resolved, header, { limit, depth
|
|
100
|
-
: renderRefs(semantic, opened, resolved, header, {
|
|
144
|
+
? renderCallers(semantic, opened, resolved, header, { ...o, limit, depth })
|
|
145
|
+
: renderRefs(semantic, opened, resolved, header, { ...o, limit, kind });
|
|
146
|
+
}
|
|
147
|
+
const OUTSIDE_ROWS = 5;
|
|
148
|
+
/**
|
|
149
|
+
* No References under an explicit Project: importers of the declaring file (or of a barrel
|
|
150
|
+
* re-exporting it) elsewhere in the workspace, from the syntactic importer index.
|
|
151
|
+
*/
|
|
152
|
+
function outsideImporters(opened, o) {
|
|
153
|
+
if (!o.explicit)
|
|
154
|
+
return { fields: {}, help: [] };
|
|
155
|
+
const workspace = detectWorkspace(opened.root);
|
|
156
|
+
if (!isMultiProject(workspace.kind))
|
|
157
|
+
return { fields: {}, help: [] };
|
|
158
|
+
const index = importerIndexFor(opened.index, workspace);
|
|
159
|
+
const closure = importerClosure(index, [o.absFile], REFS_HOPS);
|
|
160
|
+
const inProject = new Set(opened.index.files);
|
|
161
|
+
const barrels = new Set(closure.barrels);
|
|
162
|
+
const targets = [o.absFile, ...closure.barrels];
|
|
163
|
+
const needle = o.symbol.split(".").pop();
|
|
164
|
+
const rows = closure.importers
|
|
165
|
+
.filter((f) => !inProject.has(f) && !barrels.has(f) && (opened.index.text(f) ?? "").includes(needle))
|
|
166
|
+
.map((f) => {
|
|
167
|
+
const line = targets.flatMap((t) => index.importersOf(t)).find((i) => i.file === f)?.line ?? 1;
|
|
168
|
+
return { file: opened.index.rel(f), line };
|
|
169
|
+
})
|
|
170
|
+
.sort((a, b) => byDepthThenPath(a.file, b.file));
|
|
171
|
+
if (rows.length === 0)
|
|
172
|
+
return { fields: {}, help: [] };
|
|
173
|
+
return {
|
|
174
|
+
fields: {
|
|
175
|
+
found_outside_project: rows.slice(0, OUTSIDE_ROWS),
|
|
176
|
+
...(rows.length > OUTSIDE_ROWS ? { found_outside_total: rows.length } : {}),
|
|
177
|
+
hint: `${rows.length} files outside the selected project import it - run without -P for workspace scope`,
|
|
178
|
+
},
|
|
179
|
+
help: [`Run \`ts-axi ${o.name} ${o.label}\` without -P to search the whole workspace`],
|
|
180
|
+
};
|
|
101
181
|
}
|
|
102
182
|
function sortRows(rows) {
|
|
103
183
|
return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.col - b.col);
|
|
@@ -113,32 +193,72 @@ function footer(semantic, started, touched) {
|
|
|
113
193
|
const diagnostics = semantic.diagnosticsCount(touched);
|
|
114
194
|
return { elapsed_ms: Math.round(performance.now() - started), ...(diagnostics > 0 ? { diagnostics } : {}) };
|
|
115
195
|
}
|
|
196
|
+
/** `via_interface` / `via_implementations` counts, plus the hint to see their rows. */
|
|
197
|
+
function viaFields(via, bases) {
|
|
198
|
+
const base = via.filter((v) => v.through === "base");
|
|
199
|
+
const impl = via.filter((v) => v.through === "implementation");
|
|
200
|
+
return {
|
|
201
|
+
...(bases.length > 0 || base.length > 0 ? { via_interface: viaLine(base.length, [...bases, ...base.map((v) => v.via)]) } : {}),
|
|
202
|
+
...(impl.length > 0 ? { via_implementations: viaLine(impl.length, impl.map((v) => v.via)) } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/** Direct rows, plus via rows (with a `via` column) under `--via`. */
|
|
206
|
+
function mergedRows(direct, via, show) {
|
|
207
|
+
return show.via ? sortRows([...direct, ...via]) : direct;
|
|
208
|
+
}
|
|
116
209
|
function renderRefs(semantic, opened, resolved, header, o) {
|
|
117
|
-
const
|
|
210
|
+
const split = semantic.referenceSplit(resolved.absFile, resolved.pos);
|
|
211
|
+
const rows = sortRows([...split.direct]);
|
|
212
|
+
const via = sortRows([...split.via]);
|
|
213
|
+
const impls = sortRows([...split.implementations]);
|
|
118
214
|
const { files, tests, fileList } = summary(opened, rows);
|
|
119
|
-
const
|
|
120
|
-
const
|
|
215
|
+
const listed = mergedRows(rows, via, o.show);
|
|
216
|
+
const shown = listed.slice(0, o.limit);
|
|
217
|
+
const truncated = shown.length < listed.length;
|
|
121
218
|
const help = [];
|
|
122
219
|
if (truncated) {
|
|
123
220
|
help.push(o.limit < MAX_LIMIT
|
|
124
|
-
? `Showing ${shown.length} of ${
|
|
125
|
-
: `Showing ${shown.length} of ${
|
|
221
|
+
? `Showing ${shown.length} of ${listed.length}; run \`ts-axi refs ${o.declaredAt}${o.show.via ? " --via" : ""} --limit ${Math.min(MAX_LIMIT, listed.length)}\` for more`
|
|
222
|
+
: `Showing ${shown.length} of ${listed.length}; narrow by reading one file at a time`);
|
|
126
223
|
}
|
|
224
|
+
if (via.length > 0 && !o.show.via)
|
|
225
|
+
help.push(`Run \`ts-axi refs ${o.declaredAt} --via\` for the ${via.length} via References (${viaNames(via)})`);
|
|
226
|
+
if (impls.length > 0 && !o.show.implementations)
|
|
227
|
+
help.push(`Run \`ts-axi refs ${o.declaredAt} --implementations\` to list the ${impls.length} implementations`);
|
|
228
|
+
const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
|
|
229
|
+
help.unshift(...outside.help);
|
|
127
230
|
if (isFunctionLike(o.kind))
|
|
128
231
|
help.push(`Run \`ts-axi callers ${o.declaredAt}\` for call sites with their enclosing function`);
|
|
129
232
|
help.push(`Run \`ts-axi def ${o.declaredAt}\` for the declaration body`);
|
|
233
|
+
help.push(...scopeHelp(o.scope));
|
|
130
234
|
return out(block({
|
|
131
235
|
...header,
|
|
132
236
|
refs_total: rows.length,
|
|
133
237
|
files,
|
|
134
238
|
tests,
|
|
135
|
-
...(
|
|
239
|
+
...viaFields(via, split.bases),
|
|
240
|
+
...(impls.length > 0
|
|
241
|
+
? { implementations: o.show.implementations ? impls.map((r) => ({ file: r.file, line: r.line, symbol: r.name })) : impls.length }
|
|
242
|
+
: {}),
|
|
243
|
+
...(shown.length === 0
|
|
136
244
|
? {}
|
|
137
|
-
: {
|
|
245
|
+
: {
|
|
246
|
+
refs: shown.map((r) => ({
|
|
247
|
+
file: r.file,
|
|
248
|
+
line: r.line,
|
|
249
|
+
...(o.show.via ? { via: r.via ?? "-" } : {}),
|
|
250
|
+
context: contextCell(opened, r),
|
|
251
|
+
})),
|
|
252
|
+
}),
|
|
138
253
|
...(truncated ? { truncated: true } : {}),
|
|
254
|
+
...outside.fields,
|
|
255
|
+
...scopeFields(o.scope),
|
|
139
256
|
...footer(semantic, o.started, fileList),
|
|
140
257
|
}), helpBlock(help));
|
|
141
258
|
}
|
|
259
|
+
function viaNames(via) {
|
|
260
|
+
return [...new Set(via.map((v) => v.via))].join("; ");
|
|
261
|
+
}
|
|
142
262
|
function itemKey(site) {
|
|
143
263
|
return `${site.item.file}#${site.item.selectionSpan.start}`;
|
|
144
264
|
}
|
|
@@ -170,15 +290,17 @@ function depthRows(sites) {
|
|
|
170
290
|
function renderCallers(semantic, opened, resolved, header, o) {
|
|
171
291
|
const sites = semantic.incomingCalls(resolved.absFile, resolved.pos);
|
|
172
292
|
if (sites === undefined) {
|
|
173
|
-
return out(block({ ...header, refs_total: 0, note: "not callable - no call hierarchy for this Symbol", ...footer(semantic, o.started, []) }), helpBlock([`Run \`ts-axi refs ${o.declaredAt}\` for every Reference`]));
|
|
293
|
+
return out(block({ ...header, refs_total: 0, note: "not callable - no call hierarchy for this Symbol", ...scopeFields(o.scope), ...footer(semantic, o.started, []) }), helpBlock([`Run \`ts-axi refs ${o.declaredAt}\` for every Reference`]));
|
|
174
294
|
}
|
|
175
|
-
const rows = sortRows(
|
|
295
|
+
const rows = sortRows(sites.filter((s) => s.via === undefined));
|
|
296
|
+
const via = sortRows(sites.filter((s) => s.via !== undefined));
|
|
176
297
|
const { files, tests, fileList } = summary(opened, rows);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
298
|
+
const listed = mergedRows(rows, via, o.show);
|
|
299
|
+
const shown = listed.slice(0, o.limit);
|
|
300
|
+
const truncated = shown.length < listed.length;
|
|
179
301
|
const levels = {};
|
|
180
302
|
const visited = new Set();
|
|
181
|
-
let level =
|
|
303
|
+
let level = listed;
|
|
182
304
|
for (let d = 2; d <= o.depth; d++) {
|
|
183
305
|
level = nextLevel(semantic, level, visited);
|
|
184
306
|
const agg = depthRows(level);
|
|
@@ -190,22 +312,38 @@ function renderCallers(semantic, opened, resolved, header, o) {
|
|
|
190
312
|
}
|
|
191
313
|
const help = [];
|
|
192
314
|
if (truncated) {
|
|
193
|
-
help.push(`Showing ${shown.length} of ${
|
|
315
|
+
help.push(`Showing ${shown.length} of ${listed.length}; run \`ts-axi callers ${o.declaredAt}${o.show.via ? " --via" : ""} --limit ${Math.min(MAX_LIMIT, listed.length)}\` for more`);
|
|
194
316
|
}
|
|
317
|
+
if (via.length > 0 && !o.show.via)
|
|
318
|
+
help.push(`Run \`ts-axi callers ${o.declaredAt} --via\` for the ${via.length} via calls (${viaNames(via)})`);
|
|
195
319
|
const top = shown.find((r) => r.caller !== "(top-level)");
|
|
196
320
|
if (top && o.depth === 1)
|
|
197
321
|
help.push(`Run \`ts-axi callers ${o.declaredAt} --depth 2\` for who calls the callers`);
|
|
322
|
+
const outside = rows.length === 0 ? outsideImporters(opened, o) : { fields: {}, help: [] };
|
|
323
|
+
help.unshift(...outside.help);
|
|
198
324
|
help.push(`Run \`ts-axi refs ${o.declaredAt}\` for every Reference (not only calls)`);
|
|
325
|
+
help.push(...scopeHelp(o.scope));
|
|
199
326
|
return out(block({
|
|
200
327
|
...header,
|
|
201
328
|
refs_total: rows.length,
|
|
202
329
|
files,
|
|
203
330
|
tests,
|
|
204
|
-
...(
|
|
331
|
+
...viaFields(via, []),
|
|
332
|
+
...(shown.length === 0
|
|
205
333
|
? {}
|
|
206
|
-
: {
|
|
334
|
+
: {
|
|
335
|
+
refs: shown.map((r) => ({
|
|
336
|
+
file: r.file,
|
|
337
|
+
line: r.line,
|
|
338
|
+
caller: r.caller,
|
|
339
|
+
...(o.show.via ? { via: r.via ?? "-" } : {}),
|
|
340
|
+
context: contextCell(opened, r),
|
|
341
|
+
})),
|
|
342
|
+
}),
|
|
207
343
|
...(truncated ? { truncated: true } : {}),
|
|
208
344
|
...levels,
|
|
345
|
+
...outside.fields,
|
|
346
|
+
...scopeFields(o.scope),
|
|
209
347
|
...footer(semantic, o.started, fileList),
|
|
210
348
|
}), helpBlock(help));
|
|
211
349
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Syntactic reverse-import graph (regex over file text, no AST): which workspace files import
|
|
2
|
+
// a given file, through relative paths, `paths` aliases, workspace package names and barrels.
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
const PATTERNS = [
|
|
5
|
+
{ re: /\bexport\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*(['"])([^'"\n]+)\1/g, reexport: true },
|
|
6
|
+
{ re: /\bimport\s+(?:type\s+)?[^'";]*?\bfrom\s*(['"])([^'"\n]+)\1/g, reexport: false },
|
|
7
|
+
{ re: /\bimport\s*(['"])([^'"\n]+)\1/g, reexport: false },
|
|
8
|
+
{ re: /\b(?:require|import|(?:vi|jest)\.(?:mock|doMock|requireActual|importActual))\s*\(\s*(['"])([^'"\n]+)\1/g, reexport: false },
|
|
9
|
+
];
|
|
10
|
+
/** Static specifiers of `import`/`export ... from`, side-effect imports, `require()`, `import()`, `vi.mock()`. */
|
|
11
|
+
export function extractImports(text) {
|
|
12
|
+
if (!/\b(?:import|export|require|mock)\b/.test(text))
|
|
13
|
+
return [];
|
|
14
|
+
const edges = new Map();
|
|
15
|
+
let lineStarts;
|
|
16
|
+
const lineAt = (offset) => {
|
|
17
|
+
lineStarts ??= [0, ...[...text.matchAll(/\n/g)].map((m) => m.index + 1)];
|
|
18
|
+
let lo = 0;
|
|
19
|
+
let hi = lineStarts.length - 1;
|
|
20
|
+
while (lo < hi) {
|
|
21
|
+
const mid = (lo + hi + 1) >> 1;
|
|
22
|
+
if (lineStarts[mid] <= offset)
|
|
23
|
+
lo = mid;
|
|
24
|
+
else
|
|
25
|
+
hi = mid - 1;
|
|
26
|
+
}
|
|
27
|
+
return lo + 1;
|
|
28
|
+
};
|
|
29
|
+
for (const { re, reexport } of PATTERNS) {
|
|
30
|
+
for (const m of text.matchAll(re)) {
|
|
31
|
+
const specOffset = m.index + m[0].lastIndexOf(m[2]);
|
|
32
|
+
// A position matched by an earlier (re-export) pattern keeps that classification.
|
|
33
|
+
if (!edges.has(specOffset))
|
|
34
|
+
edges.set(specOffset, { spec: m[2], reexport, line: lineAt(specOffset) });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...edges.entries()].sort((a, b) => a[0] - b[0]).map(([, e]) => e);
|
|
38
|
+
}
|
|
39
|
+
const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
40
|
+
/** Resolves a specifier to a file of `files` (absolute paths); anything else (node_modules, builtins) is undefined. */
|
|
41
|
+
export function createResolver(input, files) {
|
|
42
|
+
const tryPath = (p, depth = 0) => {
|
|
43
|
+
if (files.has(p))
|
|
44
|
+
return p;
|
|
45
|
+
const stem = p.replace(/\.(?:d\.)?[mc]?[jt]sx?$/, "");
|
|
46
|
+
for (const ext of EXTENSIONS)
|
|
47
|
+
if (files.has(stem + ext))
|
|
48
|
+
return stem + ext;
|
|
49
|
+
for (const ext of EXTENSIONS)
|
|
50
|
+
if (files.has(`${p}/index${ext}`))
|
|
51
|
+
return `${p}/index${ext}`;
|
|
52
|
+
// Build output in a package manifest: look for the source next to it.
|
|
53
|
+
if (depth === 0 && /[\\/](?:dist|build|lib|out)[\\/]/.test(p))
|
|
54
|
+
return tryPath(p.replace(/([\\/])(?:dist|build|lib|out)([\\/])/, "$1src$2"), 1);
|
|
55
|
+
return undefined;
|
|
56
|
+
};
|
|
57
|
+
const aliases = Object.entries(input.paths?.paths ?? {})
|
|
58
|
+
.map(([pattern, targets]) => {
|
|
59
|
+
const star = pattern.indexOf("*");
|
|
60
|
+
return { pattern, targets, prefix: star < 0 ? pattern : pattern.slice(0, star), suffix: star < 0 ? "" : pattern.slice(star + 1), wildcard: star >= 0 };
|
|
61
|
+
})
|
|
62
|
+
// Exact patterns first, then the longest prefix (TypeScript's order).
|
|
63
|
+
.sort((a, b) => Number(a.wildcard) - Number(b.wildcard) || b.prefix.length - a.prefix.length);
|
|
64
|
+
const aliasBase = input.paths?.base ?? "";
|
|
65
|
+
const fromAlias = (spec) => {
|
|
66
|
+
for (const a of aliases) {
|
|
67
|
+
let captured;
|
|
68
|
+
if (!a.wildcard) {
|
|
69
|
+
if (spec !== a.pattern)
|
|
70
|
+
continue;
|
|
71
|
+
captured = "";
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (!spec.startsWith(a.prefix) || !spec.endsWith(a.suffix) || spec.length < a.prefix.length + a.suffix.length)
|
|
75
|
+
continue;
|
|
76
|
+
captured = spec.slice(a.prefix.length, spec.length - a.suffix.length);
|
|
77
|
+
}
|
|
78
|
+
for (const target of a.targets) {
|
|
79
|
+
const hit = tryPath(resolve(aliasBase, target.replace("*", captured)));
|
|
80
|
+
if (hit)
|
|
81
|
+
return hit;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
86
|
+
const packages = [...input.packages].sort((a, b) => b.name.length - a.name.length);
|
|
87
|
+
const fromPackage = (spec) => {
|
|
88
|
+
const pkg = packages.find((p) => spec === p.name || spec.startsWith(`${p.name}/`));
|
|
89
|
+
if (!pkg)
|
|
90
|
+
return undefined;
|
|
91
|
+
const sub = spec === pkg.name ? "." : `./${spec.slice(pkg.name.length + 1)}`;
|
|
92
|
+
const candidates = [...(pkg.exports[sub] ?? [])];
|
|
93
|
+
for (const [key, targets] of Object.entries(pkg.exports)) {
|
|
94
|
+
const star = key.indexOf("*");
|
|
95
|
+
if (star < 0 || !sub.startsWith(key.slice(0, star)) || !sub.endsWith(key.slice(star + 1)))
|
|
96
|
+
continue;
|
|
97
|
+
const captured = sub.slice(star, sub.length - (key.length - star - 1));
|
|
98
|
+
candidates.push(...targets.map((t) => t.replace("*", captured)));
|
|
99
|
+
}
|
|
100
|
+
if (sub !== ".")
|
|
101
|
+
candidates.push(resolve(pkg.dir, sub), resolve(pkg.dir, "src", sub));
|
|
102
|
+
for (const c of candidates) {
|
|
103
|
+
const hit = tryPath(c);
|
|
104
|
+
if (hit)
|
|
105
|
+
return hit;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
return (spec, fromFile) => {
|
|
110
|
+
if (spec.startsWith(".") || spec.startsWith("/"))
|
|
111
|
+
return tryPath(resolve(dirname(fromFile), spec));
|
|
112
|
+
return fromAlias(spec) ?? fromPackage(spec);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export const IMPORTER_MAX_FILES = 50_000;
|
|
116
|
+
export const IMPORTER_MAX_MS = 3_000;
|
|
117
|
+
/** Reads every candidate once (bounded) and inverts its resolved imports. */
|
|
118
|
+
export function buildImporterIndex(files, read, resolveSpec, options = {}) {
|
|
119
|
+
const maxFiles = options.maxFiles ?? IMPORTER_MAX_FILES;
|
|
120
|
+
const deadline = performance.now() + (options.maxMs ?? IMPORTER_MAX_MS);
|
|
121
|
+
const reverse = new Map();
|
|
122
|
+
let scanned = 0;
|
|
123
|
+
let partial = false;
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
if (scanned >= maxFiles || performance.now() > deadline) {
|
|
126
|
+
partial = true;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
scanned++;
|
|
130
|
+
const text = read(file);
|
|
131
|
+
if (text === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const edge of extractImports(text)) {
|
|
135
|
+
const target = resolveSpec(edge.spec, file);
|
|
136
|
+
if (!target || target === file)
|
|
137
|
+
continue;
|
|
138
|
+
const key = `${target}#${edge.reexport}`;
|
|
139
|
+
if (seen.has(key))
|
|
140
|
+
continue;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
const list = reverse.get(target) ?? [];
|
|
143
|
+
list.push({ file, reexport: edge.reexport, line: edge.line });
|
|
144
|
+
reverse.set(target, list);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { scanned, total: files.length, partial, importersOf: (file) => reverse.get(file) ?? [] };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Importers of `seeds` up to `hops` levels; a re-export (barrel) does not use a hop, so
|
|
151
|
+
* importers of a barrel that re-exports a seed count as direct importers.
|
|
152
|
+
*/
|
|
153
|
+
export function importerClosure(index, seeds, hops) {
|
|
154
|
+
const seedSet = new Set(seeds);
|
|
155
|
+
const importers = new Set();
|
|
156
|
+
const barrels = new Set();
|
|
157
|
+
let frontier = [...seeds];
|
|
158
|
+
const expanded = new Set(seeds);
|
|
159
|
+
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
160
|
+
const next = [];
|
|
161
|
+
const queue = [...frontier];
|
|
162
|
+
while (queue.length > 0) {
|
|
163
|
+
const file = queue.pop();
|
|
164
|
+
for (const imp of index.importersOf(file)) {
|
|
165
|
+
if (seedSet.has(imp.file))
|
|
166
|
+
continue;
|
|
167
|
+
importers.add(imp.file);
|
|
168
|
+
if (imp.reexport)
|
|
169
|
+
barrels.add(imp.file);
|
|
170
|
+
if (expanded.has(imp.file))
|
|
171
|
+
continue;
|
|
172
|
+
expanded.add(imp.file);
|
|
173
|
+
if (imp.reexport)
|
|
174
|
+
queue.push(imp.file);
|
|
175
|
+
else
|
|
176
|
+
next.push(imp.file);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
frontier = next;
|
|
180
|
+
}
|
|
181
|
+
return { importers: [...importers].sort(), barrels: [...barrels].sort() };
|
|
182
|
+
}
|