diffninja 0.1.0
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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Cli } from "incur";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the user asked incur to count or slice the output by tokens.
|
|
5
|
+
*
|
|
6
|
+
* These globals act on the value a command *returns*, so the ASCII fast path
|
|
7
|
+
* (which writes to stdout itself) has to opt out of them — otherwise the flags
|
|
8
|
+
* silently do nothing. See `emitAsciiOrData`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hasTokenFlag(argv: string[]): boolean;
|
|
11
|
+
/** Strip lone `--` so `calldiff a b -- src` still works with incur. */
|
|
12
|
+
export declare function normalizeArgv(argv: string[]): string[];
|
|
13
|
+
export declare const cli: Cli.Cli<{
|
|
14
|
+
diff: {
|
|
15
|
+
args: {
|
|
16
|
+
from?: string | undefined;
|
|
17
|
+
to?: string | undefined;
|
|
18
|
+
paths?: string[] | undefined;
|
|
19
|
+
};
|
|
20
|
+
options: {
|
|
21
|
+
maxDepth: number;
|
|
22
|
+
locs: boolean;
|
|
23
|
+
entry?: string | string[] | undefined;
|
|
24
|
+
file?: string | string[] | undefined;
|
|
25
|
+
from?: string | undefined;
|
|
26
|
+
to?: string | undefined;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
} & {
|
|
30
|
+
tree: {
|
|
31
|
+
args: {
|
|
32
|
+
ref?: string | undefined;
|
|
33
|
+
paths?: string[] | undefined;
|
|
34
|
+
};
|
|
35
|
+
options: {
|
|
36
|
+
maxDepth: number;
|
|
37
|
+
locs: boolean;
|
|
38
|
+
entry?: string | string[] | undefined;
|
|
39
|
+
file?: string | string[] | undefined;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
} & {
|
|
43
|
+
reach: {
|
|
44
|
+
args: {
|
|
45
|
+
ref?: string | undefined;
|
|
46
|
+
paths?: string[] | undefined;
|
|
47
|
+
};
|
|
48
|
+
options: {
|
|
49
|
+
to: string;
|
|
50
|
+
maxDepth: number;
|
|
51
|
+
locs: boolean;
|
|
52
|
+
entry?: string | string[] | undefined;
|
|
53
|
+
file?: string | string[] | undefined;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}, undefined, undefined, undefined>;
|
|
57
|
+
export default cli;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { Cli, z } from "incur";
|
|
6
|
+
import { runDiff, runReach, runTree } from "./run.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
function readVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
11
|
+
// SAFETY: package.json is authored by this repo; only version is read.
|
|
12
|
+
const pkg = raw;
|
|
13
|
+
return pkg.version ?? "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const TOKEN_FLAGS = ["--token-count", "--token-limit", "--token-offset"];
|
|
20
|
+
/**
|
|
21
|
+
* Whether the user asked incur to count or slice the output by tokens.
|
|
22
|
+
*
|
|
23
|
+
* These globals act on the value a command *returns*, so the ASCII fast path
|
|
24
|
+
* (which writes to stdout itself) has to opt out of them — otherwise the flags
|
|
25
|
+
* silently do nothing. See `emitAsciiOrData`.
|
|
26
|
+
*/
|
|
27
|
+
export function hasTokenFlag(argv) {
|
|
28
|
+
return argv.some((token) => TOKEN_FLAGS.some((flag) => token === flag || token.startsWith(`${flag}=`)));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set from the argv actually being served. incur does not pass the raw argv to
|
|
32
|
+
* command handlers, and `normalizeArgv` is the one funnel every entry point
|
|
33
|
+
* (the CLI below, and the tests) already runs argv through.
|
|
34
|
+
*/
|
|
35
|
+
let tokenFlagActive = false;
|
|
36
|
+
/** Strip lone `--` so `calldiff a b -- src` still works with incur. */
|
|
37
|
+
export function normalizeArgv(argv) {
|
|
38
|
+
tokenFlagActive = hasTokenFlag(argv);
|
|
39
|
+
return argv.filter((token) => token !== "--");
|
|
40
|
+
}
|
|
41
|
+
function entriesFromOption(entry) {
|
|
42
|
+
if (entry === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Default: classic ASCII (TTY + pipes) with optional CTAs.
|
|
48
|
+
* `--format` / `--json`: structured envelope for agents.
|
|
49
|
+
*
|
|
50
|
+
* The ASCII path writes to stdout itself so that piped output stays raw text
|
|
51
|
+
* (incur wraps a scalar payload as `{ data, cta }` once a CTA is attached).
|
|
52
|
+
* `--token-count` / `--token-limit` / `--token-offset` act on what a command
|
|
53
|
+
* *returns*, though, so under those flags we hand the ASCII back to incur and
|
|
54
|
+
* let it do the counting and slicing. The CTA is dropped there on purpose: it
|
|
55
|
+
* keeps the counted output exactly the tree the user asked about.
|
|
56
|
+
*/
|
|
57
|
+
function emitAsciiOrData(c, result, cta) {
|
|
58
|
+
if (!c.formatExplicit) {
|
|
59
|
+
if (tokenFlagActive)
|
|
60
|
+
return result.ascii;
|
|
61
|
+
process.stdout.write(result.ascii.endsWith("\n") ? result.ascii : `${result.ascii}\n`);
|
|
62
|
+
if (cta)
|
|
63
|
+
return c.ok({}, cta);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
const entryOption = z
|
|
69
|
+
.union([z.string(), z.array(z.string())])
|
|
70
|
+
.describe("Entrypoint symbol(s): functionName or ClassName.method");
|
|
71
|
+
const fileOption = z
|
|
72
|
+
.union([z.string(), z.array(z.string())])
|
|
73
|
+
.describe("Entrypoint file(s): indexed source path; expands to that file's exports");
|
|
74
|
+
const maxDepthOption = z.coerce
|
|
75
|
+
.number()
|
|
76
|
+
.default(12)
|
|
77
|
+
.describe("Max call-tree depth");
|
|
78
|
+
const locsOption = z
|
|
79
|
+
.boolean()
|
|
80
|
+
.default(false)
|
|
81
|
+
.describe("Show call-site source locations (file:line)");
|
|
82
|
+
const pathsArg = z
|
|
83
|
+
.array(z.string())
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Limit to these path prefixes");
|
|
86
|
+
export const cli = Cli.create("calldiff", {
|
|
87
|
+
description: "Diff call stacks across git commits for agentic code review (23 languages)",
|
|
88
|
+
version: readVersion(),
|
|
89
|
+
hint: "Commands: diff (compare two trees), tree (view one tree), reach (paths between symbols). Path filters are trailing positionals (a leading -- is also accepted). Use --format json for structured agent output.",
|
|
90
|
+
sync: {
|
|
91
|
+
// One skill file covering all commands (default incur depth is 1 = per-command).
|
|
92
|
+
depth: 0,
|
|
93
|
+
suggestions: [
|
|
94
|
+
"Diff HEAD against my working tree with calldiff diff",
|
|
95
|
+
"View the call tree for createAgentSession with calldiff tree",
|
|
96
|
+
"Find call paths from runCheckout to sendEmail with calldiff reach",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.command("diff", {
|
|
101
|
+
description: "Diff call stacks between two git trees",
|
|
102
|
+
args: z.object({
|
|
103
|
+
from: z.string().optional().describe("Before ref (default: HEAD)"),
|
|
104
|
+
to: z.string().optional().describe("After ref (default: working tree)"),
|
|
105
|
+
paths: pathsArg,
|
|
106
|
+
}),
|
|
107
|
+
options: z.object({
|
|
108
|
+
entry: entryOption.optional(),
|
|
109
|
+
file: fileOption.optional(),
|
|
110
|
+
maxDepth: maxDepthOption,
|
|
111
|
+
locs: locsOption,
|
|
112
|
+
from: z.string().optional().describe('Left / "before" tree'),
|
|
113
|
+
to: z.string().optional().describe('Right / "after" tree'),
|
|
114
|
+
}),
|
|
115
|
+
alias: { entry: "e", file: "F" },
|
|
116
|
+
examples: [
|
|
117
|
+
{ description: "HEAD vs working tree" },
|
|
118
|
+
{
|
|
119
|
+
description: "One ref vs working tree",
|
|
120
|
+
args: { from: "main" },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
description: "Two commits / branches",
|
|
124
|
+
args: { from: "abc123", to: "def456" },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
description: "Force entrypoints",
|
|
128
|
+
args: { from: "main", to: "feature" },
|
|
129
|
+
options: { entry: "createAgentSession" },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
description: "File as entrypoint (all exports in that file)",
|
|
133
|
+
args: { from: "main", to: "feature" },
|
|
134
|
+
options: { file: "src/routes.ts" },
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
usage: [
|
|
138
|
+
{},
|
|
139
|
+
{ args: { from: true } },
|
|
140
|
+
{ args: { from: true, to: true } },
|
|
141
|
+
{
|
|
142
|
+
args: { from: true, to: true, paths: true },
|
|
143
|
+
options: { entry: true },
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
hint: "Semantics match git diff: no refs → HEAD vs worktree; one ref → that vs worktree; two refs → compare those trees.",
|
|
147
|
+
run(c) {
|
|
148
|
+
const entries = entriesFromOption(c.options.entry);
|
|
149
|
+
const files = entriesFromOption(c.options.file);
|
|
150
|
+
let result;
|
|
151
|
+
try {
|
|
152
|
+
result = runDiff({
|
|
153
|
+
from: c.options.from ?? c.args.from,
|
|
154
|
+
to: c.options.to ?? c.args.to,
|
|
155
|
+
entries,
|
|
156
|
+
files,
|
|
157
|
+
paths: c.args.paths,
|
|
158
|
+
maxDepth: c.options.maxDepth,
|
|
159
|
+
locs: c.options.locs,
|
|
160
|
+
color: !c.formatExplicit && !c.agent,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return c.error({
|
|
165
|
+
code: "DIFF_FAILED",
|
|
166
|
+
message: error instanceof Error ? error.message : String(error),
|
|
167
|
+
exitCode: 1,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const cta = result.trees.length > 0
|
|
171
|
+
? {
|
|
172
|
+
cta: {
|
|
173
|
+
commands: result.trees.slice(0, 3).map((t) => ({
|
|
174
|
+
command: "tree",
|
|
175
|
+
options: { entry: t.entry },
|
|
176
|
+
description: `View full call tree for ${t.entry}`,
|
|
177
|
+
})),
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
: undefined;
|
|
181
|
+
return emitAsciiOrData(c, result, cta);
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
.command("tree", {
|
|
185
|
+
description: "View a call tree (no diff) for one or more entrypoints",
|
|
186
|
+
args: z.object({
|
|
187
|
+
ref: z
|
|
188
|
+
.string()
|
|
189
|
+
.optional()
|
|
190
|
+
.describe("Git ref (default: working tree)"),
|
|
191
|
+
paths: pathsArg,
|
|
192
|
+
}),
|
|
193
|
+
options: z.object({
|
|
194
|
+
entry: entryOption.optional(),
|
|
195
|
+
file: fileOption.optional(),
|
|
196
|
+
maxDepth: maxDepthOption,
|
|
197
|
+
locs: locsOption,
|
|
198
|
+
}),
|
|
199
|
+
alias: { entry: "e", file: "F" },
|
|
200
|
+
examples: [
|
|
201
|
+
{
|
|
202
|
+
description: "Tree from working tree",
|
|
203
|
+
options: { entry: "createAgentSession" },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
description: "Tree from a commit",
|
|
207
|
+
args: { ref: "HEAD" },
|
|
208
|
+
options: { entry: "PiService.createAgentSession" },
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
description: "Tree for every export in a file",
|
|
212
|
+
options: { file: "packages/api/src/routes.ts" },
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
run(c) {
|
|
216
|
+
const entries = entriesFromOption(c.options.entry) ?? [];
|
|
217
|
+
const files = entriesFromOption(c.options.file) ?? [];
|
|
218
|
+
if (entries.length === 0 && files.length === 0) {
|
|
219
|
+
return c.error({
|
|
220
|
+
code: "MISSING_ENTRY",
|
|
221
|
+
message: "calldiff tree requires --entry / -e or --file / -F",
|
|
222
|
+
exitCode: 2,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const result = runTree({
|
|
227
|
+
ref: c.args.ref,
|
|
228
|
+
entries,
|
|
229
|
+
files,
|
|
230
|
+
paths: c.args.paths,
|
|
231
|
+
maxDepth: c.options.maxDepth,
|
|
232
|
+
locs: c.options.locs,
|
|
233
|
+
color: !c.formatExplicit && !c.agent,
|
|
234
|
+
});
|
|
235
|
+
return emitAsciiOrData(c, result);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return c.error({
|
|
239
|
+
code: "TREE_FAILED",
|
|
240
|
+
message: error instanceof Error ? error.message : String(error),
|
|
241
|
+
exitCode: 1,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
.command("reach", {
|
|
247
|
+
description: "Find all call paths from an entrypoint to a target symbol",
|
|
248
|
+
args: z.object({
|
|
249
|
+
ref: z
|
|
250
|
+
.string()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Git ref (default: working tree)"),
|
|
253
|
+
paths: pathsArg,
|
|
254
|
+
}),
|
|
255
|
+
options: z.object({
|
|
256
|
+
entry: entryOption.optional(),
|
|
257
|
+
file: fileOption.optional(),
|
|
258
|
+
to: z
|
|
259
|
+
.string()
|
|
260
|
+
.describe("Target symbol to reach (functionName or ClassName.method)"),
|
|
261
|
+
maxDepth: maxDepthOption,
|
|
262
|
+
locs: locsOption,
|
|
263
|
+
}),
|
|
264
|
+
alias: { entry: "e", file: "F" },
|
|
265
|
+
examples: [
|
|
266
|
+
{
|
|
267
|
+
description: "Paths in the working tree",
|
|
268
|
+
options: { entry: "runCheckout", to: "sendEmail" },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
description: "Paths at a commit, limited to a directory",
|
|
272
|
+
args: { ref: "HEAD", paths: ["examples/checkout"] },
|
|
273
|
+
options: { entry: "runCheckout", to: "sendEmail" },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
description: "Paths from every export in a file",
|
|
277
|
+
options: { file: "packages/api/src/flow.ts", to: "notify" },
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
run(c) {
|
|
281
|
+
const entries = entriesFromOption(c.options.entry) ?? [];
|
|
282
|
+
const files = entriesFromOption(c.options.file) ?? [];
|
|
283
|
+
if (entries.length === 0 && files.length === 0) {
|
|
284
|
+
return c.error({
|
|
285
|
+
code: "MISSING_ENTRY",
|
|
286
|
+
message: "calldiff reach requires --entry / -e or --file / -F",
|
|
287
|
+
exitCode: 2,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (!c.options.to) {
|
|
291
|
+
return c.error({
|
|
292
|
+
code: "MISSING_TARGET",
|
|
293
|
+
message: "calldiff reach requires --to <symbol>",
|
|
294
|
+
exitCode: 2,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const result = runReach({
|
|
299
|
+
ref: c.args.ref,
|
|
300
|
+
entries,
|
|
301
|
+
files,
|
|
302
|
+
to: c.options.to,
|
|
303
|
+
paths: c.args.paths,
|
|
304
|
+
maxDepth: c.options.maxDepth,
|
|
305
|
+
locs: c.options.locs,
|
|
306
|
+
color: !c.formatExplicit && !c.agent,
|
|
307
|
+
});
|
|
308
|
+
return emitAsciiOrData(c, result);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return c.error({
|
|
312
|
+
code: "REACH_FAILED",
|
|
313
|
+
message: error instanceof Error ? error.message : String(error),
|
|
314
|
+
exitCode: 1,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
export default cli;
|
|
320
|
+
/** True when this file is the process entry (including npm `.bin` symlinks). */
|
|
321
|
+
function executedAsCli() {
|
|
322
|
+
const entry = process.argv[1];
|
|
323
|
+
if (!entry)
|
|
324
|
+
return false;
|
|
325
|
+
try {
|
|
326
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return (entry.endsWith("/cli.ts") ||
|
|
330
|
+
entry.endsWith("/cli.js") ||
|
|
331
|
+
entry.endsWith("\\cli.ts") ||
|
|
332
|
+
entry.endsWith("\\cli.js"));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (executedAsCli()) {
|
|
336
|
+
cli.serve(normalizeArgv(process.argv.slice(2))).catch((error) => {
|
|
337
|
+
console.error(error instanceof Error ? error.message : error);
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
});
|
|
340
|
+
}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CallNode, DiffNode } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Structural tree diff keyed by call identity.
|
|
4
|
+
* Children are aligned with LCS so output order stays close to the "after" tree.
|
|
5
|
+
*/
|
|
6
|
+
export declare function diffTrees(before: CallNode, after: CallNode): DiffNode;
|
|
7
|
+
export declare function treeHasChanges(node: DiffNode): boolean;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { pickLoc } from "./loc.js";
|
|
2
|
+
/**
|
|
3
|
+
* Structural tree diff keyed by call identity.
|
|
4
|
+
* Children are aligned with LCS so output order stays close to the "after" tree.
|
|
5
|
+
*/
|
|
6
|
+
export function diffTrees(before, after) {
|
|
7
|
+
return diffNode(before, after);
|
|
8
|
+
}
|
|
9
|
+
function diffNode(before, after) {
|
|
10
|
+
if (before && after) {
|
|
11
|
+
const node = {
|
|
12
|
+
key: after.key,
|
|
13
|
+
label: after.label,
|
|
14
|
+
kind: after.kind ?? before.kind,
|
|
15
|
+
...pickLoc(after.file ? after : before),
|
|
16
|
+
// The definition is the one `after` resolved: a call whose callee lost its
|
|
17
|
+
// indexed definition reports no definition rather than a stale before one.
|
|
18
|
+
status: "same",
|
|
19
|
+
children: diffChildren(before.children, after.children),
|
|
20
|
+
};
|
|
21
|
+
if (after.definition)
|
|
22
|
+
node.definition = after.definition;
|
|
23
|
+
return node;
|
|
24
|
+
}
|
|
25
|
+
if (after) {
|
|
26
|
+
const node = {
|
|
27
|
+
key: after.key,
|
|
28
|
+
label: after.label,
|
|
29
|
+
kind: after.kind,
|
|
30
|
+
...pickLoc(after),
|
|
31
|
+
status: "added",
|
|
32
|
+
children: after.children.map(markTree("added")),
|
|
33
|
+
};
|
|
34
|
+
if (after.definition)
|
|
35
|
+
node.definition = after.definition;
|
|
36
|
+
return node;
|
|
37
|
+
}
|
|
38
|
+
if (before) {
|
|
39
|
+
const node = {
|
|
40
|
+
key: before.key,
|
|
41
|
+
label: before.label,
|
|
42
|
+
kind: before.kind,
|
|
43
|
+
...pickLoc(before),
|
|
44
|
+
status: "removed",
|
|
45
|
+
children: before.children.map(markTree("removed")),
|
|
46
|
+
};
|
|
47
|
+
if (before.definition)
|
|
48
|
+
node.definition = before.definition;
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
throw new Error("diffNode called with no trees");
|
|
52
|
+
}
|
|
53
|
+
function markTree(status) {
|
|
54
|
+
return function mark(node) {
|
|
55
|
+
const marked = {
|
|
56
|
+
key: node.key,
|
|
57
|
+
label: node.label,
|
|
58
|
+
kind: node.kind,
|
|
59
|
+
...pickLoc(node),
|
|
60
|
+
status,
|
|
61
|
+
children: node.children.map(mark),
|
|
62
|
+
};
|
|
63
|
+
if (node.definition)
|
|
64
|
+
marked.definition = node.definition;
|
|
65
|
+
return marked;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function diffChildren(before, after) {
|
|
69
|
+
const n = before.length;
|
|
70
|
+
const m = after.length;
|
|
71
|
+
const dp = Array.from({ length: n + 1 }, () => Array.from({ length: m + 1 }, () => 0));
|
|
72
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
73
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
74
|
+
if (before[i].key === after[j].key) {
|
|
75
|
+
dp[i][j] = dp[i + 1][j + 1] + 1;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const result = [];
|
|
83
|
+
let i = 0;
|
|
84
|
+
let j = 0;
|
|
85
|
+
while (i < n && j < m) {
|
|
86
|
+
if (before[i].key === after[j].key) {
|
|
87
|
+
result.push(diffNode(before[i], after[j]));
|
|
88
|
+
i += 1;
|
|
89
|
+
j += 1;
|
|
90
|
+
}
|
|
91
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
92
|
+
result.push(diffNode(before[i], null));
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
result.push(diffNode(null, after[j]));
|
|
97
|
+
j += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
while (i < n) {
|
|
101
|
+
result.push(diffNode(before[i], null));
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
while (j < m) {
|
|
105
|
+
result.push(diffNode(null, after[j]));
|
|
106
|
+
j += 1;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
export function treeHasChanges(node) {
|
|
111
|
+
if (node.status !== "same")
|
|
112
|
+
return true;
|
|
113
|
+
return node.children.some(treeHasChanges);
|
|
114
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CallStep, FunctionInfo } from "./types.js";
|
|
2
|
+
export declare function extractFunctions(file: string, source: string): FunctionInfo[];
|
|
3
|
+
type CachedFunction = Omit<FunctionInfo, "file">;
|
|
4
|
+
export type ExtractionCache = Map<string, CachedFunction[]>;
|
|
5
|
+
export declare function extractCached(file: string, source: string, cache: ExtractionCache): FunctionInfo[];
|
|
6
|
+
export type FunctionIndex = Map<string, FunctionInfo>;
|
|
7
|
+
/** Callable and non-callable declarations available to snapshot review context. */
|
|
8
|
+
export declare function allContextDefinitions(index: FunctionIndex): FunctionInfo[];
|
|
9
|
+
/** NUL cannot occur in a path or a symbol key, so it is a safe separator. */
|
|
10
|
+
export declare function fileScopedKey(file: string, key: string): string;
|
|
11
|
+
export declare function flattenCallKeys(steps: CallStep[]): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Every function recorded when the index was built (including duplicates that
|
|
14
|
+
* lost the bare-key race). Indexes created as plain Maps fall back to values().
|
|
15
|
+
*/
|
|
16
|
+
export declare function allFunctions(index: FunctionIndex): FunctionInfo[];
|
|
17
|
+
/**
|
|
18
|
+
* Definitions of `key` declared in `file`, top-level before locals, then in
|
|
19
|
+
* source order. Empty when that file declares none.
|
|
20
|
+
*
|
|
21
|
+
* Indexes created as plain Maps (not via {@link buildIndex}) fall back to a
|
|
22
|
+
* scan of their values.
|
|
23
|
+
*/
|
|
24
|
+
export declare function definitionsInFile(index: FunctionIndex, file: string, key: string): FunctionInfo[];
|
|
25
|
+
export declare function buildIndex(functions: FunctionInfo[]): FunctionIndex;
|
|
26
|
+
export {};
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-language extraction: detect by file extension, load grammar
|
|
3
|
+
* (bundled or on-demand cache), parse, then run the language extractor.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import Parser from "tree-sitter";
|
|
7
|
+
import { loadGrammarPackage, resolveLanguage, } from "./languages/grammars.js";
|
|
8
|
+
import { detectLanguage } from "./languages/registry.js";
|
|
9
|
+
import { linesFromOffsets } from "./loc.js";
|
|
10
|
+
const parser = new Parser();
|
|
11
|
+
const languageCache = new Map();
|
|
12
|
+
function grammarKey(npmPackage, grammarExport) {
|
|
13
|
+
return grammarExport ? `${npmPackage}:${grammarExport}` : npmPackage;
|
|
14
|
+
}
|
|
15
|
+
function loadLanguage(npmPackage, grammarExport) {
|
|
16
|
+
const key = grammarKey(npmPackage, grammarExport);
|
|
17
|
+
const cached = languageCache.get(key);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
const mod = loadGrammarPackage(npmPackage);
|
|
21
|
+
const language = resolveLanguage(mod, grammarExport);
|
|
22
|
+
languageCache.set(key, language);
|
|
23
|
+
return language;
|
|
24
|
+
}
|
|
25
|
+
export function extractFunctions(file, source) {
|
|
26
|
+
const extractor = detectLanguage(file);
|
|
27
|
+
if (!extractor)
|
|
28
|
+
return [];
|
|
29
|
+
const language = loadLanguage(extractor.grammarPackage, extractor.grammarExport);
|
|
30
|
+
// Grammar package exports are accepted by setLanguage at runtime; the
|
|
31
|
+
// published Language type requires fields not present on every package.
|
|
32
|
+
// @ts-expect-error tree-sitter Language under-specifies grammar module exports
|
|
33
|
+
parser.setLanguage(language);
|
|
34
|
+
const tree = parser.parse(source);
|
|
35
|
+
return extractor.extract(file, source, tree).map((fn) => {
|
|
36
|
+
if (fn.line != null)
|
|
37
|
+
return fn;
|
|
38
|
+
const lines = linesFromOffsets(source, fn.start, fn.end);
|
|
39
|
+
return { ...fn, ...lines };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function extractCached(file, source, cache) {
|
|
43
|
+
const extractor = detectLanguage(file);
|
|
44
|
+
if (!extractor)
|
|
45
|
+
return [];
|
|
46
|
+
const digest = createHash("sha256").update(source).digest("hex");
|
|
47
|
+
const key = `${file}\0${extractor.id}\0${digest}`;
|
|
48
|
+
let functions = cache.get(key);
|
|
49
|
+
if (!functions) {
|
|
50
|
+
functions = extractFunctions(file, source).map(({ file: _ignored, ...fn }) => fn);
|
|
51
|
+
cache.set(key, functions);
|
|
52
|
+
}
|
|
53
|
+
return functions.map((fn) => ({ ...fn, file }));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Full extraction list for an index, including definitions shadowed by
|
|
57
|
+
* first-wins bare-key insertion in {@link buildIndex}.
|
|
58
|
+
*/
|
|
59
|
+
const indexDefinitions = new WeakMap();
|
|
60
|
+
const indexContextDefinitions = new WeakMap();
|
|
61
|
+
/** Callable and non-callable declarations available to snapshot review context. */
|
|
62
|
+
export function allContextDefinitions(index) {
|
|
63
|
+
return indexContextDefinitions.get(index) ?? allFunctions(index);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Definitions grouped by `file\0key`, so a call can be resolved against the
|
|
67
|
+
* file it was written in instead of the global bare-key map. See #19.
|
|
68
|
+
*/
|
|
69
|
+
const indexByFile = new WeakMap();
|
|
70
|
+
/** NUL cannot occur in a path or a symbol key, so it is a safe separator. */
|
|
71
|
+
export function fileScopedKey(file, key) {
|
|
72
|
+
return `${file}\0${key}`;
|
|
73
|
+
}
|
|
74
|
+
export function flattenCallKeys(steps) {
|
|
75
|
+
const keys = [];
|
|
76
|
+
const walk = (list) => {
|
|
77
|
+
for (const step of list) {
|
|
78
|
+
if (step.type === "call") {
|
|
79
|
+
keys.push(step.key);
|
|
80
|
+
if (step.children)
|
|
81
|
+
walk(step.children);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
walk(step.children);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
walk(steps);
|
|
89
|
+
return keys;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every function recorded when the index was built (including duplicates that
|
|
93
|
+
* lost the bare-key race). Indexes created as plain Maps fall back to values().
|
|
94
|
+
*/
|
|
95
|
+
export function allFunctions(index) {
|
|
96
|
+
return indexDefinitions.get(index) ?? [...index.values()];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Definitions of `key` declared in `file`, top-level before locals, then in
|
|
100
|
+
* source order. Empty when that file declares none.
|
|
101
|
+
*
|
|
102
|
+
* Indexes created as plain Maps (not via {@link buildIndex}) fall back to a
|
|
103
|
+
* scan of their values.
|
|
104
|
+
*/
|
|
105
|
+
export function definitionsInFile(index, file, key) {
|
|
106
|
+
const byFile = indexByFile.get(index);
|
|
107
|
+
if (byFile)
|
|
108
|
+
return byFile.get(fileScopedKey(file, key)) ?? [];
|
|
109
|
+
return [...index.values()].filter((fn) => fn.file === file && fn.key === key);
|
|
110
|
+
}
|
|
111
|
+
export function buildIndex(functions) {
|
|
112
|
+
const index = new Map();
|
|
113
|
+
const byFile = new Map();
|
|
114
|
+
const record = (fn) => {
|
|
115
|
+
const scoped = fileScopedKey(fn.file, fn.key);
|
|
116
|
+
const existing = byFile.get(scoped);
|
|
117
|
+
if (existing)
|
|
118
|
+
existing.push(fn);
|
|
119
|
+
else
|
|
120
|
+
byFile.set(scoped, [fn]);
|
|
121
|
+
if (!index.has(fn.key)) {
|
|
122
|
+
index.set(fn.key, fn);
|
|
123
|
+
}
|
|
124
|
+
if (fn.key.endsWith(".constructor")) {
|
|
125
|
+
const className = fn.key.slice(0, -".constructor".length);
|
|
126
|
+
const newKey = `new ${className}`;
|
|
127
|
+
const ctor = { ...fn, key: newKey, label: `new ${className}()` };
|
|
128
|
+
const scopedNew = fileScopedKey(fn.file, newKey);
|
|
129
|
+
const existingNew = byFile.get(scopedNew);
|
|
130
|
+
if (existingNew)
|
|
131
|
+
existingNew.push(ctor);
|
|
132
|
+
else
|
|
133
|
+
byFile.set(scopedNew, [ctor]);
|
|
134
|
+
if (!index.has(newKey)) {
|
|
135
|
+
index.set(newKey, ctor);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
// Top level first: a helper declared inside a body must never take a bare key
|
|
140
|
+
// away from a real top-level definition somewhere else in the repo (#19).
|
|
141
|
+
const callables = functions.filter(fn => !fn.review?.kind);
|
|
142
|
+
for (const fn of callables)
|
|
143
|
+
if (!fn.local)
|
|
144
|
+
record(fn);
|
|
145
|
+
for (const fn of callables)
|
|
146
|
+
if (fn.local)
|
|
147
|
+
record(fn);
|
|
148
|
+
indexDefinitions.set(index, callables);
|
|
149
|
+
indexContextDefinitions.set(index, functions.slice());
|
|
150
|
+
indexByFile.set(index, byFile);
|
|
151
|
+
return index;
|
|
152
|
+
}
|