ts-axi 0.1.11 → 0.1.13
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 +28 -3
- package/dist/src/anchor.js +113 -0
- package/dist/src/cli.js +7 -3
- package/dist/src/commands/anchor.js +250 -0
- package/dist/src/commands/common.js +47 -21
- package/dist/src/commands/def.js +4 -2
- package/dist/src/commands/impact.js +122 -25
- package/dist/src/commands/refs.js +27 -8
- package/dist/src/diff.js +20 -6
- package/dist/src/errors.js +2 -0
- package/dist/src/git.js +3 -3
- package/dist/src/skill.js +2 -1
- package/dist/src/symbols.js +93 -24
- package/dist/src/templates.js +9 -1
- package/package.json +1 -1
- package/skills/ts-axi/SKILL.md +1 -1
|
@@ -3,7 +3,7 @@ import { sourceFs } from "../fs.js";
|
|
|
3
3
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import ts from "typescript";
|
|
5
5
|
import { bool, flag, intFlag, parseArgs, parseFields } from "../args.js";
|
|
6
|
-
import { changedRanges, parseUnifiedDiff } from "../diff.js";
|
|
6
|
+
import { changedRanges, headRevOfRange, parseUnifiedDiff } from "../diff.js";
|
|
7
7
|
import { AxiError, usageError } from "../errors.js";
|
|
8
8
|
import { diffNameStatus, diffUnified, isGitRepo, resolveBaseRev, showFile, untrackedFiles } from "../git.js";
|
|
9
9
|
import { allocateBudget, classifyFile, compareChanges, formatRanges, inRanges, localOnlyLine, matchKey, reverseApply, wordMatches, } from "../impact.js";
|
|
@@ -25,8 +25,8 @@ input:
|
|
|
25
25
|
(none) = working tree vs HEAD (staged + unstaged + untracked); --staged = index vs HEAD
|
|
26
26
|
<git-range> = any git diff range (main...HEAD, abc123, v1..v2); - = unified diff on stdin (needs the after side checked out)
|
|
27
27
|
analysis is of the current checkout - for a range whose head is not the working tree, check that head out first
|
|
28
|
-
flags[
|
|
29
|
-
--staged, --via (adds References through an interface or base member to refs[n] with a via column), --depth <1-3> (callers_depthN[n]{file,count} for N > 1), --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} ref rows across the report), --grep (adds unresolved[n]{file,line,context} - textual matches that are not References; also searches .astro/.vue/.svelte/.mdx/.html), --test-glob <glob> (repeatable), --fields <kind,exported,refs_total,via,files,tests|all> (extra changed[n] columns), -P <tsconfig>, --json
|
|
28
|
+
flags[9]:
|
|
29
|
+
--staged, --size (adds size{files,hunks,lines_added,lines_removed,patch_chars,exported_signature_changes,removed_symbols} - hunks are counted at zero context for a git source, as given for -; untracked files count as added),--via (adds References through an interface or base member to refs[n] with a via column), --depth <1-3> (callers_depthN[n]{file,count} for N > 1), --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} ref rows across the report), --grep (adds unresolved[n]{file,line,context} - textual matches that are not References; also searches .astro/.vue/.svelte/.mdx/.html), --test-glob <glob> (repeatable), --fields <kind,exported,refs_total,via,files,tests|all> (extra changed[n] columns), -P <tsconfig>, --json
|
|
30
30
|
output:
|
|
31
31
|
base, head, changes (count), changed[n]{symbol,change,at,refs_outside} by default; --fields adds columns in place, --fields all gives {symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}
|
|
32
32
|
change = signature | removed | type | added | body | modified (before unavailable) - riskiest first
|
|
@@ -41,10 +41,11 @@ output:
|
|
|
41
41
|
skipped[n]{file,reason} = files in the diff that were not analysed (.astro, .json, outside the Project)
|
|
42
42
|
tests: 0 on an exported signature or body change = no Test file references it (columns from --fields exported,tests)
|
|
43
43
|
scope = in a monorepo without -P the program is the touched files + two levels of importers (scope_partial: true when capped)
|
|
44
|
-
examples[
|
|
44
|
+
examples[6]:
|
|
45
45
|
ts-axi impact
|
|
46
46
|
ts-axi impact --staged
|
|
47
47
|
ts-axi impact main...HEAD
|
|
48
|
+
ts-axi impact main...HEAD --size
|
|
48
49
|
ts-axi impact HEAD~3 --depth 2 --grep
|
|
49
50
|
bb-axi pr diff 42 --raw | ts-axi impact -
|
|
50
51
|
`;
|
|
@@ -56,14 +57,24 @@ const SPECS = [
|
|
|
56
57
|
{ name: "--grep", boolean: true },
|
|
57
58
|
{ name: "--test-glob", repeatable: true },
|
|
58
59
|
{ name: "--fields" },
|
|
60
|
+
{ name: "--size", boolean: true },
|
|
59
61
|
{ name: "-P" },
|
|
60
62
|
];
|
|
61
63
|
// Opt-in columns of changed[n]; the default keeps symbol, change, at, refs_outside.
|
|
62
64
|
const IMPACT_FIELDS = ["kind", "exported", "refs_total", "via", "files", "tests"];
|
|
63
65
|
const OPT_IN = new Set(IMPACT_FIELDS);
|
|
64
|
-
|
|
66
|
+
function diffSize(parsed, text) {
|
|
67
|
+
return {
|
|
68
|
+
files: parsed.length,
|
|
69
|
+
hunks: parsed.reduce((n, f) => n + f.hunks.length, 0),
|
|
70
|
+
lines_added: parsed.reduce((n, f) => n + f.hunks.reduce((m, h) => m + h.added.length, 0), 0),
|
|
71
|
+
lines_removed: parsed.reduce((n, f) => n + f.hunks.reduce((m, h) => m + h.removed.length, 0), 0),
|
|
72
|
+
patch_chars: text.length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export async function readStdin(command = "impact", example = "git diff -U0 | ts-axi impact -") {
|
|
65
76
|
if (process.stdin.isTTY) {
|
|
66
|
-
throw usageError(
|
|
77
|
+
throw usageError(`\`${command} -\` reads a unified diff from stdin and nothing was piped`, [example]);
|
|
67
78
|
}
|
|
68
79
|
const chunks = [];
|
|
69
80
|
for await (const chunk of process.stdin)
|
|
@@ -79,16 +90,6 @@ function readText(abs) {
|
|
|
79
90
|
return null;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
|
-
/** Right side of a range; undefined when git diffs it against the working tree (`abc123`). */
|
|
83
|
-
function headOfRange(range) {
|
|
84
|
-
const triple = range.indexOf("...");
|
|
85
|
-
if (triple > 0)
|
|
86
|
-
return range.slice(triple + 3) || "HEAD";
|
|
87
|
-
const double = range.indexOf("..");
|
|
88
|
-
if (double > 0)
|
|
89
|
-
return range.slice(double + 2) || "HEAD";
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
93
|
async function gitInput(cwd, root, range, staged) {
|
|
93
94
|
if (!(await isGitRepo(cwd))) {
|
|
94
95
|
throw new AxiError("not a git repository - impact needs git or a diff on stdin", "GIT_ERROR", [
|
|
@@ -97,8 +98,9 @@ async function gitInput(cwd, root, range, staged) {
|
|
|
97
98
|
}
|
|
98
99
|
const [diffText, names] = await Promise.all([diffUnified(cwd, range, staged), diffNameStatus(cwd, range, staged)]);
|
|
99
100
|
const parsed = parseUnifiedDiff(diffText);
|
|
101
|
+
const size = diffSize(parsed, diffText);
|
|
100
102
|
const base = staged ? "HEAD" : await resolveBaseRev(cwd, range);
|
|
101
|
-
const headRev = staged ? "" : range !== undefined ?
|
|
103
|
+
const headRev = staged ? "" : range !== undefined ? headRevOfRange(range) : undefined;
|
|
102
104
|
const files = [];
|
|
103
105
|
for (const diff of parsed) {
|
|
104
106
|
const { after, before } = changedRanges(diff);
|
|
@@ -113,16 +115,23 @@ async function gitInput(cwd, root, range, staged) {
|
|
|
113
115
|
continue;
|
|
114
116
|
const lines = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
115
117
|
files.push({ path, status: "added", after: lines > 0 ? [[1, lines]] : [], before: [], afterText: text, beforeText: null });
|
|
118
|
+
// Untracked files count as added, one hunk each; they are not in patch_chars.
|
|
119
|
+
size.files++;
|
|
120
|
+
size.hunks += lines > 0 ? 1 : 0;
|
|
121
|
+
size.lines_added += lines;
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
const seen = new Set(parsed.map((d) => d.path));
|
|
125
|
+
const noText = names.filter((n) => !seen.has(n.path) && n.status !== "deleted").map((n) => n.path);
|
|
126
|
+
size.files += noText.length;
|
|
119
127
|
return {
|
|
120
128
|
base,
|
|
121
129
|
head: staged ? "index" : headRev ?? "working tree",
|
|
122
130
|
...(headRev !== undefined ? { headRev } : {}),
|
|
123
131
|
files,
|
|
124
|
-
noText
|
|
132
|
+
noText,
|
|
125
133
|
missing: [],
|
|
134
|
+
size,
|
|
126
135
|
};
|
|
127
136
|
}
|
|
128
137
|
function stdinInput(text, root) {
|
|
@@ -139,7 +148,7 @@ function stdinInput(text, root) {
|
|
|
139
148
|
const beforeText = diff.status === "added" ? null : reverseApply(afterText ?? "", diff.hunks) ?? undefined;
|
|
140
149
|
files.push({ path: diff.path, ...(diff.oldPath ? { oldPath: diff.oldPath } : {}), status: diff.status, after, before, afterText, beforeText });
|
|
141
150
|
}
|
|
142
|
-
return { base: "stdin diff", head: "working tree", files, noText: [], missing };
|
|
151
|
+
return { base: "stdin diff", head: "working tree", files, noText: [], missing, size: diffSize(parseUnifiedDiff(text, { binary: true }), text) };
|
|
143
152
|
}
|
|
144
153
|
function isSourcePath(path) {
|
|
145
154
|
return SOURCE_EXTENSIONS.some((ext) => path.endsWith(ext));
|
|
@@ -270,6 +279,27 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
|
|
|
270
279
|
}
|
|
271
280
|
return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
272
281
|
}
|
|
282
|
+
/** Whole-word matches of `name` in the Project files, one row per line: the fallback when no Declaration matches. */
|
|
283
|
+
export function textualScan(opened, name) {
|
|
284
|
+
const rows = [];
|
|
285
|
+
sourceFs().prefetch(opened.index.files);
|
|
286
|
+
for (const abs of opened.index.files) {
|
|
287
|
+
const text = opened.index.text(abs);
|
|
288
|
+
if (text === undefined || !text.includes(name))
|
|
289
|
+
continue;
|
|
290
|
+
let last = 0;
|
|
291
|
+
for (const m of wordMatches(text, name)) {
|
|
292
|
+
if (m.line === last)
|
|
293
|
+
continue;
|
|
294
|
+
last = m.line;
|
|
295
|
+
rows.push({ file: opened.index.rel(abs), line: m.line, context: contextOf(lineOf(text, m.line), m.col) });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return rows.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
299
|
+
}
|
|
300
|
+
export function textualScanHint(label, matches) {
|
|
301
|
+
return `Run \`ts-axi refs ${label} --grep\` for ${matches} textual matches`;
|
|
302
|
+
}
|
|
273
303
|
/** Whether a whole-word count of the name says anything: not for router-called or by-name-injected members (`injected`). */
|
|
274
304
|
export function isTextualCandidate(decl) {
|
|
275
305
|
return decl?.injected !== true;
|
|
@@ -297,9 +327,66 @@ export function isUntyped(opened, decl, file) {
|
|
|
297
327
|
return false;
|
|
298
328
|
return decl?.nameless === true || opened.index.project.implicit || (file !== undefined && /\.[mc]?jsx?$/.test(file));
|
|
299
329
|
}
|
|
300
|
-
/**
|
|
301
|
-
|
|
302
|
-
|
|
330
|
+
/** The identifier starting at `pos`. */
|
|
331
|
+
function identifierAt(node, pos, sf) {
|
|
332
|
+
if (ts.isIdentifier(node) && node.getStart(sf) === pos)
|
|
333
|
+
return node;
|
|
334
|
+
return ts.forEachChild(node, (child) => (child.pos <= pos && pos < child.end ? identifierAt(child, pos, sf) : undefined));
|
|
335
|
+
}
|
|
336
|
+
/** Names a CommonJS file hands out (`module.exports = { a, b: c }`, `exports.x = d`); undefined when not CommonJS. */
|
|
337
|
+
function commonJsExports(sf) {
|
|
338
|
+
const names = new Set();
|
|
339
|
+
let cjs = /\brequire\s*\(/.test(sf.text);
|
|
340
|
+
// Values only: identifiers used inside exported function bodies do not leave the file.
|
|
341
|
+
const collect = (expr) => {
|
|
342
|
+
let e = expr;
|
|
343
|
+
while (ts.isParenthesizedExpression(e))
|
|
344
|
+
e = e.expression;
|
|
345
|
+
if (ts.isIdentifier(e))
|
|
346
|
+
names.add(e.text);
|
|
347
|
+
else if (ts.isObjectLiteralExpression(e)) {
|
|
348
|
+
for (const p of e.properties) {
|
|
349
|
+
if (ts.isShorthandPropertyAssignment(p))
|
|
350
|
+
names.add(p.name.text);
|
|
351
|
+
else if (ts.isPropertyAssignment(p))
|
|
352
|
+
collect(p.initializer);
|
|
353
|
+
else if (ts.isSpreadAssignment(p))
|
|
354
|
+
collect(p.expression);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
for (const st of sf.statements) {
|
|
359
|
+
if (!ts.isExpressionStatement(st) || !ts.isBinaryExpression(st.expression) || st.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
360
|
+
continue;
|
|
361
|
+
const left = st.expression.left.getText(sf).replace(/\s+/g, "");
|
|
362
|
+
if (!/^(module\.exports|exports)(\.|$)/.test(left))
|
|
363
|
+
continue;
|
|
364
|
+
cjs = true;
|
|
365
|
+
collect(st.expression.right);
|
|
366
|
+
}
|
|
367
|
+
return cjs ? names : undefined;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* A Declaration only its own file can name: a parameter or function-scoped binding, or a non-exported top-level
|
|
371
|
+
* binding of an ES module, or of a CommonJS file whose exports never mention it. Members, object properties,
|
|
372
|
+
* string registrations and script globals are not.
|
|
373
|
+
*/
|
|
374
|
+
export function isPlainLocal(index, decl) {
|
|
375
|
+
if (decl === undefined || decl.exported || decl.nameless || decl.parentName !== undefined)
|
|
376
|
+
return false;
|
|
377
|
+
const sf = index.sourceFile(index.abs(decl.file));
|
|
378
|
+
const id = sf && identifierAt(sf, decl.namePos, sf);
|
|
379
|
+
if (!sf || !id)
|
|
380
|
+
return false;
|
|
381
|
+
const owner = id.parent;
|
|
382
|
+
if (ts.isPropertyAssignment(owner) || ts.isShorthandPropertyAssignment(owner) || ts.isMethodDeclaration(owner) || ts.isPropertyAccessExpression(owner))
|
|
383
|
+
return false;
|
|
384
|
+
for (let n = owner.parent; !ts.isSourceFile(n); n = n.parent)
|
|
385
|
+
if (ts.isFunctionLike(n))
|
|
386
|
+
return true;
|
|
387
|
+
if (ts.isExternalModule(sf))
|
|
388
|
+
return true;
|
|
389
|
+
return commonJsExports(sf)?.has(decl.name) === false;
|
|
303
390
|
}
|
|
304
391
|
/**
|
|
305
392
|
* Whole-word occurrences of `name` in Project files outside `changeSet`; lines declaring a `name` excluded.
|
|
@@ -392,7 +479,7 @@ function capped(key, rows) {
|
|
|
392
479
|
};
|
|
393
480
|
}
|
|
394
481
|
/** Quotes a value for a pasteable command. */
|
|
395
|
-
function shellWord(value) {
|
|
482
|
+
export function shellWord(value) {
|
|
396
483
|
return /^[\w@%+=:,.\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
397
484
|
}
|
|
398
485
|
/** The same `impact` invocation with every given flag replayed, the one `extra` sets replaced by it. */
|
|
@@ -515,12 +602,22 @@ export async function impactCommand(args, ctx) {
|
|
|
515
602
|
}
|
|
516
603
|
}
|
|
517
604
|
const total = declChanges.length + fileLevel.length;
|
|
605
|
+
const exportedOf = (c) => (c.after ?? c.before)?.exported ?? false;
|
|
518
606
|
const header = {
|
|
519
607
|
...(warnings.length > 0 ? { warning: warnings.join("; ") } : {}),
|
|
520
608
|
...projectFields(opened),
|
|
521
609
|
base: input.base,
|
|
522
610
|
head: input.head,
|
|
523
611
|
changes: total,
|
|
612
|
+
...(bool(parsed, "--size")
|
|
613
|
+
? {
|
|
614
|
+
size: {
|
|
615
|
+
...input.size,
|
|
616
|
+
exported_signature_changes: declChanges.filter((c) => c.change === "signature" && exportedOf(c)).length,
|
|
617
|
+
removed_symbols: declChanges.filter((c) => c.change === "removed").length,
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
: {}),
|
|
524
621
|
};
|
|
525
622
|
const skipTemplates = (used) => {
|
|
526
623
|
for (const f of templateInputs)
|
|
@@ -592,7 +689,7 @@ export async function impactCommand(args, ctx) {
|
|
|
592
689
|
for (const row of rows) {
|
|
593
690
|
if (!row.live || !isUntyped(opened, row.live, row.live.file))
|
|
594
691
|
continue;
|
|
595
|
-
const localTo = isPlainLocal(row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
|
|
692
|
+
const localTo = isPlainLocal(opened.index, row.live) ? new Map([[row.live.name, opened.index.abs(row.live.file)]]) : undefined;
|
|
596
693
|
const needle = textualNeedle(row.live);
|
|
597
694
|
if (!needle)
|
|
598
695
|
continue;
|
|
@@ -667,7 +764,7 @@ export async function impactCommand(args, ctx) {
|
|
|
667
764
|
if (!textNames.has(needle.text))
|
|
668
765
|
textNames.set(needle.text, needle);
|
|
669
766
|
const abs = opened.index.abs(decl.file);
|
|
670
|
-
if (!isPlainLocal(decl) || (localTo.has(name) && localTo.get(name) !== abs))
|
|
767
|
+
if (!isPlainLocal(opened.index, decl) || (localTo.has(name) && localTo.get(name) !== abs))
|
|
671
768
|
wide.add(name);
|
|
672
769
|
else
|
|
673
770
|
localTo.set(name, abs);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, sep } from "node:path";
|
|
2
2
|
import { bool, expectPositionals, flag, intFlag, parseArgs } from "../args.js";
|
|
3
3
|
import { importerClosure } from "../importers.js";
|
|
4
|
-
import { isPlainLocal, isTextualCandidate, isUntyped, textualCount, textualNeedle, textualHint, textualLine, textualMatches } from "./impact.js";
|
|
4
|
+
import { isPlainLocal, isTextualCandidate, isUntyped, textualCount, textualNeedle, textualHint, textualLine, textualMatches, textualScan, textualScanHint } from "./impact.js";
|
|
5
5
|
import { findOutsideProject, nearestTsconfig } from "../project.js";
|
|
6
6
|
import { viaLine } from "../references.js";
|
|
7
7
|
import { block, helpBlock, out } from "../render.js";
|
|
@@ -10,7 +10,7 @@ import { loadTemplates, templatesLine, templateTarget, templateUsages } from "..
|
|
|
10
10
|
import { byDepthThenPath } from "../walk.js";
|
|
11
11
|
import { detectWorkspace, isMultiProject } from "../workspace.js";
|
|
12
12
|
import { isTestFile } from "../settings.js";
|
|
13
|
-
import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
|
|
13
|
+
import { isFunctionLike, kindFromScriptElement, parseTarget, registrationOf } from "../symbols.js";
|
|
14
14
|
import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, useRef, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
|
|
15
15
|
const DEFAULT_LIMIT = 50;
|
|
16
16
|
const MAX_LIMIT = 500;
|
|
@@ -23,8 +23,8 @@ const VIA_DOC = `via_interface: n (Iface.member) = References through an interfa
|
|
|
23
23
|
refs_total: 0 under -P in a workspace lists found_outside_project[n]{file,line} = importers outside that Project
|
|
24
24
|
scope = in a monorepo without -P the program is the declaring file + its importers (scope_partial: true when the importer scan was capped)
|
|
25
25
|
tsconfig: x (n referenced configs) = a references-only tsconfig expanded to the configs it references
|
|
26
|
-
textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' for a non-exported
|
|
27
|
-
templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money); refs --grep lists them in unresolved`;
|
|
26
|
+
textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' only for a real local (a parameter, a binding inside a function or IIFE, a non-exported top-level binding of an ES module or of a CommonJS file whose module.exports never hands it out) - members, object properties and script globals count Project-wide; above refs_total = uses the checker cannot resolve - see refs --grep
|
|
27
|
+
templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money) - also for a function registered by identifier (.directive('fancyBox', fancyBox)), under the registered name; refs --grep lists them in unresolved`;
|
|
28
28
|
export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
|
|
29
29
|
flags[13]:
|
|
30
30
|
${SHARED_FLAGS}
|
|
@@ -34,7 +34,9 @@ flags[13]:
|
|
|
34
34
|
output:
|
|
35
35
|
symbol, kind, declared_at, refs_total, files, tests, refs[n]{file,line,context} (Declarations excluded), elapsed_ms
|
|
36
36
|
${VIA_DOC}
|
|
37
|
-
note = the name is registered by a string ('Name' in .controller('Name', fn)) - its uses are textual; add --grep
|
|
37
|
+
note = the name is registered by a string ('Name' in .controller('Name', fn), or fn a same-file function named by identifier) - its uses are textual; add --grep
|
|
38
|
+
matches: 0 = no Declaration of the name; help counts its whole-word matches in Project files, and --grep lists them in unresolved with note 'no declaration found - textual matches only'
|
|
39
|
+
path:line = the declaration that starts the line (a function before its parameters, a property before its value's parameters; siblings like 'const a = 1, b = 2' list identifiers - add :col)
|
|
38
40
|
an ambiguous name lists matches[n] (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms; nothing is analysed
|
|
39
41
|
examples[4]:
|
|
40
42
|
ts-axi refs User.save
|
|
@@ -111,8 +113,23 @@ async function run(args, ctx, invoked) {
|
|
|
111
113
|
`Run \`ts-axi ${name} ${outside[0].file}:${outside[0].nameLine}\` to use the nearest tsconfig of that file`,
|
|
112
114
|
]));
|
|
113
115
|
}
|
|
116
|
+
// No Declaration, but the name may still be written: an untyped property set up elsewhere.
|
|
117
|
+
const text = f.fuzzy ? [] : textualScan(opened, target.member ?? target.name);
|
|
118
|
+
if (show.grep) {
|
|
119
|
+
return out(block({
|
|
120
|
+
...warning,
|
|
121
|
+
matches: 0,
|
|
122
|
+
note: "no declaration found - textual matches only",
|
|
123
|
+
unresolved: text.length === 0 ? 0 : text.slice(0, limit),
|
|
124
|
+
...(text.length > limit ? { unresolved_total: text.length } : {}),
|
|
125
|
+
}), helpBlock([
|
|
126
|
+
...(text.length > limit ? [`Showing ${limit} of ${text.length}; run \`ts-axi refs ${label} --grep --limit ${Math.min(MAX_LIMIT, text.length)}\` for more`] : []),
|
|
127
|
+
`Run \`ts-axi def ${label} --fuzzy\` for substring matches`,
|
|
128
|
+
]));
|
|
129
|
+
}
|
|
114
130
|
const suggested = suggestions(opened, target);
|
|
115
131
|
return out(block({ ...warning, matches: 0, ...(suggested.length > 0 ? { did_you_mean: suggested } : {}) }), helpBlock([
|
|
132
|
+
...(text.length > 0 ? [textualScanHint(label, text.length)] : []),
|
|
116
133
|
...suggested.slice(0, 2).map((s) => `Run \`ts-axi ${name} ${s}\``),
|
|
117
134
|
...(f.fuzzy ? [] : [`Run \`ts-axi def ${label} --fuzzy\` for substring matches`]),
|
|
118
135
|
]));
|
|
@@ -143,15 +160,17 @@ async function run(args, ctx, invoked) {
|
|
|
143
160
|
}
|
|
144
161
|
const { semantic, scope } = built;
|
|
145
162
|
const def = semantic.definitionAt(resolved.absFile, resolved.pos);
|
|
146
|
-
const
|
|
163
|
+
const found = resolved.decl ??
|
|
147
164
|
(def && def.line > 0 ? declarationAt(opened.index, def.absFile, def.pos, kindFromScriptElement(def.kind)) : undefined);
|
|
165
|
+
// A function registered by identifier is its registration: templates, note and textual use the registered name.
|
|
166
|
+
const decl = (found && registrationOf(found, opened.index.declarations(opened.index.abs(found.file)))) ?? found;
|
|
148
167
|
const kind = decl?.kind ?? kindFromScriptElement(def?.kind ?? semantic.quickInfo(resolved.absFile, resolved.pos)?.kind ?? "");
|
|
149
168
|
const symbol = decl?.qualifiedName ?? def?.name ?? label;
|
|
150
169
|
const declaredAt = decl ? `${decl.file}:${decl.nameLine}` : def && def.line > 0 ? `${def.file}:${def.line}` : "(outside the Project)";
|
|
151
170
|
const header = { ...warning, ...projectFields(opened), symbol, kind, declared_at: declaredAt };
|
|
152
171
|
const explicit = Boolean(project || opened.settings.project);
|
|
153
172
|
const declaredFile = decl?.file ?? (def && def.line > 0 ? def.file : undefined);
|
|
154
|
-
const localFile = isPlainLocal(decl) ? opened.index.abs(decl.file) : undefined;
|
|
173
|
+
const localFile = isPlainLocal(opened.index, decl) ? opened.index.abs(decl.file) : undefined;
|
|
155
174
|
const needle = textualNeedle(decl ?? symbol, kind);
|
|
156
175
|
const textual = declaredFile !== undefined && needle && isUntyped(opened, decl, declaredFile) ? textualCount(opened, needle, new Map(), localFile) : undefined;
|
|
157
176
|
const tplTarget = decl ? templateTarget(decl, opened.index.declarations(opened.index.abs(decl.file))) : undefined;
|
|
@@ -249,7 +268,7 @@ function unresolvedOf(opened, semantic, o, known) {
|
|
|
249
268
|
const needle = isTextualCandidate(o.decl) ? textualNeedle(o.decl ?? o.symbol, o.kind) : undefined;
|
|
250
269
|
if (!needle)
|
|
251
270
|
return [];
|
|
252
|
-
return textualMatches(opened, semantic, [needle], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map(), isPlainLocal(o.decl) ? new Map([[o.decl.name, opened.index.abs(o.decl.file)]]) : new Map());
|
|
271
|
+
return textualMatches(opened, semantic, [needle], new Set(known.map((r) => `${r.file}:${r.line}`)), new Set(o.decl ? [`${o.decl.file}:${o.decl.line}`, `${o.decl.file}:${o.decl.nameLine}`] : []), new Map(), isPlainLocal(opened.index, o.decl) ? new Map([[o.decl.name, opened.index.abs(o.decl.file)]]) : new Map());
|
|
253
272
|
}
|
|
254
273
|
/**
|
|
255
274
|
* `textual` field and, when the name occurs more often than refs_total and some matches sit on no
|
package/dist/src/diff.js
CHANGED
|
@@ -40,8 +40,8 @@ function parseGitHeader(rest) {
|
|
|
40
40
|
const m = /^"?a\/(.+?)"? "?b\/(.+?)"?$/.exec(rest);
|
|
41
41
|
return m ? [m[1], m[2]] : [undefined, undefined];
|
|
42
42
|
}
|
|
43
|
-
function finish(draft) {
|
|
44
|
-
if (draft.binary)
|
|
43
|
+
function finish(draft, keepBinary) {
|
|
44
|
+
if (draft.binary && !keepBinary)
|
|
45
45
|
return undefined;
|
|
46
46
|
const isNew = draft.newMode || draft.minus === null;
|
|
47
47
|
const isDeleted = draft.deletedMode || draft.plus === null;
|
|
@@ -62,13 +62,15 @@ function finish(draft) {
|
|
|
62
62
|
oldPath: status === "renamed" ? oldP : undefined,
|
|
63
63
|
status,
|
|
64
64
|
hunks: draft.hunks,
|
|
65
|
+
...(draft.binary ? { binary: true } : {}),
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
|
-
* Parse git-style unified diff text. Binary files are skipped; files
|
|
69
|
-
* hunks (pure renames, mode changes) are kept with `hunks: []`.
|
|
69
|
+
* Parse git-style unified diff text. Binary files are skipped unless `binary` is set; files
|
|
70
|
+
* with no hunks (pure renames, mode changes) are kept with `hunks: []`. Splits on `\n` only,
|
|
71
|
+
* so `\r`, U+2028 or U+2029 inside a line never shift the numbering.
|
|
70
72
|
*/
|
|
71
|
-
export function parseUnifiedDiff(text) {
|
|
73
|
+
export function parseUnifiedDiff(text, options = {}) {
|
|
72
74
|
const files = [];
|
|
73
75
|
let draft;
|
|
74
76
|
let hunk;
|
|
@@ -78,7 +80,7 @@ export function parseUnifiedDiff(text) {
|
|
|
78
80
|
let newNo = 0;
|
|
79
81
|
const flush = () => {
|
|
80
82
|
if (draft) {
|
|
81
|
-
const file = finish(draft);
|
|
83
|
+
const file = finish(draft, options.binary === true);
|
|
82
84
|
if (file)
|
|
83
85
|
files.push(file);
|
|
84
86
|
}
|
|
@@ -104,6 +106,7 @@ export function parseUnifiedDiff(text) {
|
|
|
104
106
|
continue;
|
|
105
107
|
}
|
|
106
108
|
if (tag === " " || line === "") {
|
|
109
|
+
hunk.context.push({ oldLine: oldNo, newLine: newNo });
|
|
107
110
|
oldNo++;
|
|
108
111
|
newNo++;
|
|
109
112
|
oldLeft--;
|
|
@@ -130,6 +133,7 @@ export function parseUnifiedDiff(text) {
|
|
|
130
133
|
newLines: h[4] === undefined ? 1 : Number(h[4]),
|
|
131
134
|
removed: [],
|
|
132
135
|
added: [],
|
|
136
|
+
context: [],
|
|
133
137
|
};
|
|
134
138
|
draft.hunks.push(hunk);
|
|
135
139
|
oldLeft = hunk.oldLines;
|
|
@@ -211,3 +215,13 @@ export function baseRevOfRange(range) {
|
|
|
211
215
|
return range;
|
|
212
216
|
return range.slice(0, index) || "HEAD";
|
|
213
217
|
}
|
|
218
|
+
/** Right side of a range; undefined when git diffs it against the working tree (`abc123`). */
|
|
219
|
+
export function headRevOfRange(range) {
|
|
220
|
+
const triple = range.indexOf("...");
|
|
221
|
+
if (triple > 0)
|
|
222
|
+
return range.slice(triple + 3) || "HEAD";
|
|
223
|
+
const double = range.indexOf("..");
|
|
224
|
+
if (double > 0)
|
|
225
|
+
return range.slice(double + 2) || "HEAD";
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
package/dist/src/errors.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AxiError, exitCodeForError as sdkExitCode } from "axi-sdk-js";
|
|
2
2
|
export { AxiError };
|
|
3
|
+
/** `anchor` exit code when an anchor is not commentable; set by the handler, the output still prints. */
|
|
4
|
+
export const ANCHOR_INVALID_EXIT = 3;
|
|
3
5
|
/** SDK mapping: 2 for usage errors, 1 otherwise (`GIT_ERROR` included). */
|
|
4
6
|
export function exitCodeForError(error) {
|
|
5
7
|
return sdkExitCode(error);
|
package/dist/src/git.js
CHANGED
|
@@ -155,10 +155,10 @@ export async function untrackedFiles(cwd) {
|
|
|
155
155
|
const stdout = await runOrThrow(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--full-name", ":/"]);
|
|
156
156
|
return stdout.split("\0").filter((p) => p !== "");
|
|
157
157
|
}
|
|
158
|
-
/** Raw unified diff with
|
|
159
|
-
export async function diffUnified(cwd, range, staged) {
|
|
158
|
+
/** Raw unified diff with `context` lines around each change (default 0); same target rules as `diffNameStatus`. */
|
|
159
|
+
export async function diffUnified(cwd, range, staged, context = 0) {
|
|
160
160
|
const target = await diffTarget(cwd, range, staged);
|
|
161
|
-
return runOrThrow(cwd, ["diff",
|
|
161
|
+
return runOrThrow(cwd, ["diff", `-U${context}`, "--no-color", "--no-ext-diff", "-M", ...target, "--"]);
|
|
162
162
|
}
|
|
163
163
|
/** File content at `rev`, or `null` when the path does not exist there. */
|
|
164
164
|
export async function showFile(cwd, rev, path) {
|
package/dist/src/skill.js
CHANGED
|
@@ -3,7 +3,8 @@ import { DESCRIPTION } from "./cli.js";
|
|
|
3
3
|
// outcome-focused so it fires on code-navigation and review intents.
|
|
4
4
|
export const SKILL_DESCRIPTION = "Navigate TypeScript and JavaScript code and measure change impact through the ts-axi CLI - find where a symbol is declared, " +
|
|
5
5
|
"list its references and callers, outline a file, and see which other code a diff affects. Use before reading many files to find " +
|
|
6
|
-
"callers or usages of a function, class or type; use `ts-axi impact` when reviewing a diff, a pull request or your own uncommitted changes
|
|
6
|
+
"callers or usages of a function, class or type; use `ts-axi impact` when reviewing a diff, a pull request or your own uncommitted changes, " +
|
|
7
|
+
"and `ts-axi anchor` to check that review-comment file:line anchors are in the diff before posting them.";
|
|
7
8
|
// Hard cap so a regeneration cannot re-inflate the stub with CLI-owned
|
|
8
9
|
// guidance. The home view and --help output are the source of truth.
|
|
9
10
|
export const MAX_SKILL_MARKDOWN_CHARS = 2000;
|