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/git.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Snapshot, SnapshotPair, SnapshotPairWithPaths, SnapshotWithPaths } from "./types.js";
|
|
2
|
+
export declare function assertGitRepo(cwd: string): void;
|
|
3
|
+
export declare function resolveSnapshots(from: string | undefined, to: string | undefined): SnapshotPair;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve diff from/to/paths with git-diff defaults, treating on-disk path
|
|
6
|
+
* positionals as path filters when they are not valid git refs.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* - `diff main src` → main vs worktree, paths=[src]
|
|
10
|
+
* - `diff main feature src` → main vs feature, paths=[src]
|
|
11
|
+
* - `diff src` → HEAD vs worktree, paths=[src]
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveDiffSnapshotsAndPaths(cwd: string, from: string | undefined, to: string | undefined, paths: string[]): SnapshotPairWithPaths;
|
|
14
|
+
/** Single snapshot for `calldiff tree` / `reach` — no ref → working tree. */
|
|
15
|
+
export declare function resolveSnapshot(ref: string | undefined): Snapshot;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve optional ref + path filters for tree/reach.
|
|
18
|
+
* A lone positional that isn't a git ref but exists on disk is treated as a
|
|
19
|
+
* path filter on the working tree (`calldiff tree -e foo src/lib`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveSnapshotAndPaths(cwd: string, ref: string | undefined, paths: string[]): SnapshotWithPaths;
|
|
22
|
+
export declare function verifyCommit(cwd: string, ref: string): void;
|
|
23
|
+
export interface SnapshotFile {
|
|
24
|
+
path: string;
|
|
25
|
+
/** Git blob metadata. Worktree files do not have it. */
|
|
26
|
+
oid?: string;
|
|
27
|
+
size?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function listSnapshotFiles(cwd: string, snapshot: Snapshot, pathFilters?: string[]): SnapshotFile[];
|
|
30
|
+
export declare function visitCommitBlobs(cwd: string, files: SnapshotFile[], visit: (oid: string, source: string) => void): void;
|
|
31
|
+
export declare function visitWorktreeFiles(cwd: string, files: SnapshotFile[], visit: (file: SnapshotFile, source: string) => void): void;
|
|
32
|
+
export declare function describeSnapshot(snapshot: Snapshot): string;
|
|
33
|
+
/**
|
|
34
|
+
* Read one file from a snapshot, or null when it does not exist there.
|
|
35
|
+
*
|
|
36
|
+
* A commit is read through git, so the text is the immutable blob the revision
|
|
37
|
+
* recorded even when the worktree has moved on. A worktree snapshot reads the
|
|
38
|
+
* file on disk.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readSnapshotFile(cwd: string, snapshot: Snapshot, path: string): string | null;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { listSupportedExtensions } from "./languages/registry.js";
|
|
5
|
+
function gitBuffer(cwd, args, input, maxBuffer = 64 * 1024 * 1024) {
|
|
6
|
+
return execFileSync("git", ["--no-replace-objects", ...args], {
|
|
7
|
+
cwd,
|
|
8
|
+
input,
|
|
9
|
+
maxBuffer,
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function git(cwd, args) {
|
|
14
|
+
return gitBuffer(cwd, args).toString("utf8");
|
|
15
|
+
}
|
|
16
|
+
export function assertGitRepo(cwd) {
|
|
17
|
+
try {
|
|
18
|
+
git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error(`Not a git repository: ${cwd}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function resolveSnapshots(from, to) {
|
|
25
|
+
// git-diff defaults: no args → HEAD vs worktree; one arg → that vs worktree
|
|
26
|
+
const left = {
|
|
27
|
+
kind: "commit",
|
|
28
|
+
ref: from ?? "HEAD",
|
|
29
|
+
};
|
|
30
|
+
const right = to === undefined
|
|
31
|
+
? { kind: "worktree", ref: "WORKTREE" }
|
|
32
|
+
: { kind: "commit", ref: to };
|
|
33
|
+
return { from: left, to: right };
|
|
34
|
+
}
|
|
35
|
+
function isCommitRef(cwd, ref) {
|
|
36
|
+
try {
|
|
37
|
+
git(cwd, ["rev-parse", "--verify", `${ref}^{commit}`]);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isPathOnDisk(cwd, value) {
|
|
45
|
+
return existsSync(resolve(cwd, value));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve diff from/to/paths with git-diff defaults, treating on-disk path
|
|
49
|
+
* positionals as path filters when they are not valid git refs.
|
|
50
|
+
*
|
|
51
|
+
* Examples:
|
|
52
|
+
* - `diff main src` → main vs worktree, paths=[src]
|
|
53
|
+
* - `diff main feature src` → main vs feature, paths=[src]
|
|
54
|
+
* - `diff src` → HEAD vs worktree, paths=[src]
|
|
55
|
+
*/
|
|
56
|
+
export function resolveDiffSnapshotsAndPaths(cwd, from, to, paths) {
|
|
57
|
+
if (from === undefined && to === undefined) {
|
|
58
|
+
return { ...resolveSnapshots(undefined, undefined), paths };
|
|
59
|
+
}
|
|
60
|
+
if (from !== undefined && to === undefined) {
|
|
61
|
+
if (isCommitRef(cwd, from)) {
|
|
62
|
+
return { ...resolveSnapshots(from, undefined), paths };
|
|
63
|
+
}
|
|
64
|
+
if (isPathOnDisk(cwd, from)) {
|
|
65
|
+
return { ...resolveSnapshots(undefined, undefined), paths: [from, ...paths] };
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Unknown git ref: ${from}`);
|
|
68
|
+
}
|
|
69
|
+
if (from !== undefined && to !== undefined) {
|
|
70
|
+
if (!isCommitRef(cwd, from)) {
|
|
71
|
+
throw new Error(`Unknown git ref: ${from}`);
|
|
72
|
+
}
|
|
73
|
+
if (isCommitRef(cwd, to)) {
|
|
74
|
+
return { ...resolveSnapshots(from, to), paths };
|
|
75
|
+
}
|
|
76
|
+
if (isPathOnDisk(cwd, to)) {
|
|
77
|
+
return {
|
|
78
|
+
...resolveSnapshots(from, undefined),
|
|
79
|
+
paths: [to, ...paths],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Unknown git ref: ${to}`);
|
|
83
|
+
}
|
|
84
|
+
// to without from shouldn't happen via CLI positionals, but honor options.
|
|
85
|
+
return { ...resolveSnapshots(from, to), paths };
|
|
86
|
+
}
|
|
87
|
+
/** Single snapshot for `calldiff tree` / `reach` — no ref → working tree. */
|
|
88
|
+
export function resolveSnapshot(ref) {
|
|
89
|
+
if (ref === undefined) {
|
|
90
|
+
return { kind: "worktree", ref: "WORKTREE" };
|
|
91
|
+
}
|
|
92
|
+
return { kind: "commit", ref };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve optional ref + path filters for tree/reach.
|
|
96
|
+
* A lone positional that isn't a git ref but exists on disk is treated as a
|
|
97
|
+
* path filter on the working tree (`calldiff tree -e foo src/lib`).
|
|
98
|
+
*/
|
|
99
|
+
export function resolveSnapshotAndPaths(cwd, ref, paths) {
|
|
100
|
+
if (ref === undefined) {
|
|
101
|
+
return { snapshot: resolveSnapshot(undefined), paths };
|
|
102
|
+
}
|
|
103
|
+
if (isCommitRef(cwd, ref)) {
|
|
104
|
+
return { snapshot: resolveSnapshot(ref), paths };
|
|
105
|
+
}
|
|
106
|
+
if (isPathOnDisk(cwd, ref)) {
|
|
107
|
+
return {
|
|
108
|
+
snapshot: resolveSnapshot(undefined),
|
|
109
|
+
paths: [ref, ...paths],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Unknown git ref: ${ref}`);
|
|
113
|
+
}
|
|
114
|
+
export function verifyCommit(cwd, ref) {
|
|
115
|
+
if (!isCommitRef(cwd, ref)) {
|
|
116
|
+
throw new Error(`Unknown git ref: ${ref}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const SOURCE_EXT = new Set(listSupportedExtensions());
|
|
120
|
+
function isSourceFile(path) {
|
|
121
|
+
const lower = path.toLowerCase();
|
|
122
|
+
if (lower.endsWith(".d.ts"))
|
|
123
|
+
return false;
|
|
124
|
+
const dot = lower.lastIndexOf(".");
|
|
125
|
+
if (dot < 0)
|
|
126
|
+
return false;
|
|
127
|
+
return SOURCE_EXT.has(lower.slice(dot));
|
|
128
|
+
}
|
|
129
|
+
function isRegularFile(path) {
|
|
130
|
+
try {
|
|
131
|
+
return lstatSync(path).isFile();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function listWorktreeFiles(cwd) {
|
|
138
|
+
const output = git(cwd, [
|
|
139
|
+
"ls-files",
|
|
140
|
+
"-z",
|
|
141
|
+
"--cached",
|
|
142
|
+
"--others",
|
|
143
|
+
"--exclude-standard",
|
|
144
|
+
]);
|
|
145
|
+
return output
|
|
146
|
+
.split("\0")
|
|
147
|
+
.filter((path) => path && isSourceFile(path) && isRegularFile(resolve(cwd, path)))
|
|
148
|
+
.map((path) => ({ path }));
|
|
149
|
+
}
|
|
150
|
+
function listCommitFiles(cwd, ref) {
|
|
151
|
+
const output = git(cwd, ["ls-tree", "-r", "-z", "-l", ref]);
|
|
152
|
+
const files = [];
|
|
153
|
+
for (const record of output.split("\0")) {
|
|
154
|
+
if (!record)
|
|
155
|
+
continue;
|
|
156
|
+
const tab = record.indexOf("\t");
|
|
157
|
+
if (tab < 0)
|
|
158
|
+
continue;
|
|
159
|
+
const [mode, type, oid, sizeText] = record
|
|
160
|
+
.slice(0, tab)
|
|
161
|
+
.trim()
|
|
162
|
+
.split(/\s+/);
|
|
163
|
+
const path = record.slice(tab + 1);
|
|
164
|
+
const regular = mode === "100644" || mode === "100755";
|
|
165
|
+
const size = Number(sizeText);
|
|
166
|
+
if (regular &&
|
|
167
|
+
type === "blob" &&
|
|
168
|
+
oid &&
|
|
169
|
+
Number.isInteger(size) &&
|
|
170
|
+
isSourceFile(path)) {
|
|
171
|
+
files.push({ path, oid, size });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return files;
|
|
175
|
+
}
|
|
176
|
+
function pathAllowed(file, pathFilters) {
|
|
177
|
+
if (pathFilters.length === 0)
|
|
178
|
+
return true;
|
|
179
|
+
return pathFilters.some((filter) => {
|
|
180
|
+
const normalized = filter.replace(/^\.\//, "").replace(/\/$/, "");
|
|
181
|
+
return (file === normalized ||
|
|
182
|
+
file.startsWith(`${normalized}/`) ||
|
|
183
|
+
file.endsWith(normalized));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
export function listSnapshotFiles(cwd, snapshot, pathFilters = []) {
|
|
187
|
+
const files = snapshot.kind === "worktree"
|
|
188
|
+
? listWorktreeFiles(cwd)
|
|
189
|
+
: listCommitFiles(cwd, snapshot.ref);
|
|
190
|
+
return files
|
|
191
|
+
.filter((file) => pathAllowed(file.path, pathFilters))
|
|
192
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
193
|
+
}
|
|
194
|
+
const BATCH_BYTES = 32 * 1024 * 1024;
|
|
195
|
+
function chunkBlobs(files) {
|
|
196
|
+
const unique = new Map();
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
if (file.oid && file.size !== undefined && !unique.has(file.oid)) {
|
|
199
|
+
unique.set(file.oid, file.size);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const chunks = [];
|
|
203
|
+
let chunk = [];
|
|
204
|
+
let bytes = 0;
|
|
205
|
+
for (const [oid, size] of unique) {
|
|
206
|
+
if (chunk.length > 0 && bytes + size > BATCH_BYTES) {
|
|
207
|
+
chunks.push(chunk);
|
|
208
|
+
chunk = [];
|
|
209
|
+
bytes = 0;
|
|
210
|
+
}
|
|
211
|
+
chunk.push({ oid, size });
|
|
212
|
+
bytes += size;
|
|
213
|
+
}
|
|
214
|
+
if (chunk.length > 0)
|
|
215
|
+
chunks.push(chunk);
|
|
216
|
+
return chunks;
|
|
217
|
+
}
|
|
218
|
+
function readBlobBatch(cwd, batch) {
|
|
219
|
+
const input = Buffer.from(`${batch.map((blob) => blob.oid).join("\n")}\n`);
|
|
220
|
+
const bytes = batch.reduce((total, blob) => total + blob.size, 0);
|
|
221
|
+
const maxBuffer = bytes + batch.length * 128 + 1024;
|
|
222
|
+
const output = gitBuffer(cwd, ["cat-file", "--batch"], input, maxBuffer);
|
|
223
|
+
const blobs = new Map();
|
|
224
|
+
let offset = 0;
|
|
225
|
+
while (offset < output.length) {
|
|
226
|
+
const headerEnd = output.indexOf(0x0a, offset);
|
|
227
|
+
if (headerEnd < 0)
|
|
228
|
+
throw new Error("Invalid git cat-file response");
|
|
229
|
+
const header = output.subarray(offset, headerEnd).toString("ascii");
|
|
230
|
+
const [oid, type, sizeText] = header.split(" ");
|
|
231
|
+
const size = Number(sizeText);
|
|
232
|
+
if (!oid || type !== "blob" || !Number.isInteger(size)) {
|
|
233
|
+
throw new Error(`Invalid git cat-file header: ${header}`);
|
|
234
|
+
}
|
|
235
|
+
const contentStart = headerEnd + 1;
|
|
236
|
+
const contentEnd = contentStart + size;
|
|
237
|
+
if (contentEnd >= output.length) {
|
|
238
|
+
throw new Error(`Truncated git blob: ${oid}`);
|
|
239
|
+
}
|
|
240
|
+
blobs.set(oid, output.subarray(contentStart, contentEnd).toString("utf8"));
|
|
241
|
+
offset = contentEnd + 1;
|
|
242
|
+
}
|
|
243
|
+
return blobs;
|
|
244
|
+
}
|
|
245
|
+
export function visitCommitBlobs(cwd, files, visit) {
|
|
246
|
+
for (const batch of chunkBlobs(files)) {
|
|
247
|
+
for (const [oid, source] of readBlobBatch(cwd, batch)) {
|
|
248
|
+
visit(oid, source);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export function visitWorktreeFiles(cwd, files, visit) {
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
const full = resolve(cwd, file.path);
|
|
255
|
+
if (!isRegularFile(full))
|
|
256
|
+
continue;
|
|
257
|
+
visit(file, readFileSync(full, "utf8"));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export function describeSnapshot(snapshot) {
|
|
261
|
+
return snapshot.kind === "worktree" ? "working tree" : snapshot.ref;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read one file from a snapshot, or null when it does not exist there.
|
|
265
|
+
*
|
|
266
|
+
* A commit is read through git, so the text is the immutable blob the revision
|
|
267
|
+
* recorded even when the worktree has moved on. A worktree snapshot reads the
|
|
268
|
+
* file on disk.
|
|
269
|
+
*/
|
|
270
|
+
export function readSnapshotFile(cwd, snapshot, path) {
|
|
271
|
+
if (snapshot.kind === "worktree") {
|
|
272
|
+
const full = resolve(cwd, path);
|
|
273
|
+
if (!isRegularFile(full))
|
|
274
|
+
return null;
|
|
275
|
+
try {
|
|
276
|
+
return readFileSync(full, "utf8");
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
return git(cwd, ["show", `${snapshot.ref}:${path}`]);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { cli, normalizeArgv } from "./cli.js";
|
|
2
|
+
export { buildCallTree, exportsInFile, matchEntrypointFiles, normalizeEntryPath, resolveAllEntries, resolveEntry, resolveEntrypointFile, resolveFileEntrypoints, } from "./calltree.js";
|
|
3
|
+
export { diffTrees, treeHasChanges } from "./diff.js";
|
|
4
|
+
export { allFunctions, buildIndex, definitionsInFile, extractFunctions, } from "./extract.js";
|
|
5
|
+
export { formatSourceLoc, pickLoc } from "./loc.js";
|
|
6
|
+
export { collectPathsTo, findReachPaths, pathToTree } from "./reach.js";
|
|
7
|
+
export { renderDiff, renderTree } from "./render.js";
|
|
8
|
+
export { runDiff, runReach, runTree } from "./run.js";
|
|
9
|
+
export type * from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { cli, normalizeArgv } from "./cli.js";
|
|
2
|
+
export { buildCallTree, exportsInFile, matchEntrypointFiles, normalizeEntryPath, resolveAllEntries, resolveEntry, resolveEntrypointFile, resolveFileEntrypoints, } from "./calltree.js";
|
|
3
|
+
export { diffTrees, treeHasChanges } from "./diff.js";
|
|
4
|
+
export { allFunctions, buildIndex, definitionsInFile, extractFunctions, } from "./extract.js";
|
|
5
|
+
export { formatSourceLoc, pickLoc } from "./loc.js";
|
|
6
|
+
export { collectPathsTo, findReachPaths, pathToTree } from "./reach.js";
|
|
7
|
+
export { renderDiff, renderTree } from "./render.js";
|
|
8
|
+
export { runDiff, runReach, runTree } from "./run.js";
|
package/dist/infer.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FunctionIndex } from "./extract.js";
|
|
2
|
+
import type { DiffNode, FunctionInfo } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Infer entrypoints: exported functions whose expanded call trees differ,
|
|
5
|
+
* plus any explicitly requested symbol entries.
|
|
6
|
+
*/
|
|
7
|
+
export declare function inferEntries(before: FunctionIndex, after: FunctionIndex, explicit: string[], maxDepth: number): string[];
|
|
8
|
+
export type ExplicitDiffEntry = {
|
|
9
|
+
key: string;
|
|
10
|
+
beforeInfo?: FunctionInfo;
|
|
11
|
+
afterInfo?: FunctionInfo;
|
|
12
|
+
/** When set, trees are built from these file-pinned definitions. */
|
|
13
|
+
file?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Expand explicit `-e` symbols and `--file` paths into concrete diff roots.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveExplicitDiffEntries(before: FunctionIndex, after: FunctionIndex, symbols: string[], files?: string[]): ExplicitDiffEntry[];
|
|
19
|
+
export declare function diffEntry(key: string, before: FunctionIndex, after: FunctionIndex, maxDepth: number): DiffNode | null;
|
|
20
|
+
/** Diff two file-pinned definitions (or missing on one side). */
|
|
21
|
+
export declare function diffPinnedEntry(key: string, beforeInfo: FunctionInfo | undefined, afterInfo: FunctionInfo | undefined, before: FunctionIndex, after: FunctionIndex, maxDepth: number): DiffNode | null;
|
package/dist/infer.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { allFunctions, flattenCallKeys } from "./extract.js";
|
|
2
|
+
import { buildCallTree, buildCallTreeFromInfo, exportsInFile, indexedFiles, resolveEntry, resolveEntrypointFile, } from "./calltree.js";
|
|
3
|
+
import { diffTrees, treeHasChanges } from "./diff.js";
|
|
4
|
+
function emptyCallTree(key, label) {
|
|
5
|
+
return { key, label, children: [] };
|
|
6
|
+
}
|
|
7
|
+
function functionFingerprint(fn) {
|
|
8
|
+
if (!fn)
|
|
9
|
+
return null;
|
|
10
|
+
return JSON.stringify({ label: fn.label, steps: fn.steps });
|
|
11
|
+
}
|
|
12
|
+
function addReverseEdges(reverse, index) {
|
|
13
|
+
for (const [caller, fn] of index) {
|
|
14
|
+
for (const callee of flattenCallKeys(fn.steps)) {
|
|
15
|
+
let callers = reverse.get(callee);
|
|
16
|
+
if (!callers) {
|
|
17
|
+
callers = new Set();
|
|
18
|
+
reverse.set(callee, callers);
|
|
19
|
+
}
|
|
20
|
+
callers.add(caller);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function findAffected(before, after, maxDepth) {
|
|
25
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
26
|
+
const reverse = new Map();
|
|
27
|
+
addReverseEdges(reverse, before);
|
|
28
|
+
addReverseEdges(reverse, after);
|
|
29
|
+
const distance = new Map();
|
|
30
|
+
const queue = [];
|
|
31
|
+
for (const key of keys) {
|
|
32
|
+
if (functionFingerprint(before.get(key)) === functionFingerprint(after.get(key))) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
distance.set(key, 0);
|
|
36
|
+
queue.push(key);
|
|
37
|
+
}
|
|
38
|
+
for (let i = 0; i < queue.length; i += 1) {
|
|
39
|
+
const callee = queue[i];
|
|
40
|
+
const nextDistance = distance.get(callee) + 1;
|
|
41
|
+
if (nextDistance > maxDepth)
|
|
42
|
+
continue;
|
|
43
|
+
for (const caller of reverse.get(callee) ?? []) {
|
|
44
|
+
const previous = distance.get(caller);
|
|
45
|
+
if (previous !== undefined && previous <= nextDistance)
|
|
46
|
+
continue;
|
|
47
|
+
distance.set(caller, nextDistance);
|
|
48
|
+
queue.push(caller);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return new Set(distance.keys());
|
|
52
|
+
}
|
|
53
|
+
function changedKeys(keys, before, after, maxDepth) {
|
|
54
|
+
return keys
|
|
55
|
+
.filter((key) => diffEntry(key, before, after, maxDepth) !== null)
|
|
56
|
+
.sort((a, b) => a.localeCompare(b));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Infer entrypoints: exported functions whose expanded call trees differ,
|
|
60
|
+
* plus any explicitly requested symbol entries.
|
|
61
|
+
*/
|
|
62
|
+
export function inferEntries(before, after, explicit, maxDepth) {
|
|
63
|
+
if (explicit.length > 0) {
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const entry of explicit) {
|
|
66
|
+
const key = resolveEntry(entry, after) ?? resolveEntry(entry, before);
|
|
67
|
+
if (!key)
|
|
68
|
+
throw new Error(`Entrypoint not found: ${entry}`);
|
|
69
|
+
if (!entries.includes(key))
|
|
70
|
+
entries.push(key);
|
|
71
|
+
}
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
const affected = [...findAffected(before, after, maxDepth)].filter((key) => !key.startsWith("new "));
|
|
75
|
+
const exported = affected.filter((key) => Boolean(before.get(key)?.exported || after.get(key)?.exported));
|
|
76
|
+
const entries = changedKeys(exported, before, after, maxDepth);
|
|
77
|
+
if (entries.length > 0)
|
|
78
|
+
return entries;
|
|
79
|
+
const checked = new Set(exported);
|
|
80
|
+
const fallback = affected.filter((key) => !checked.has(key));
|
|
81
|
+
return changedKeys(fallback, before, after, maxDepth);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Expand explicit `-e` symbols and `--file` paths into concrete diff roots.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveExplicitDiffEntries(before, after, symbols, files = []) {
|
|
87
|
+
const out = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const entry of symbols) {
|
|
90
|
+
const key = resolveEntry(entry, after) ?? resolveEntry(entry, before);
|
|
91
|
+
if (!key)
|
|
92
|
+
throw new Error(`Entrypoint not found: ${entry}`);
|
|
93
|
+
if (seen.has(key))
|
|
94
|
+
continue;
|
|
95
|
+
seen.add(key);
|
|
96
|
+
out.push({ key });
|
|
97
|
+
}
|
|
98
|
+
for (const entry of files) {
|
|
99
|
+
const file = resolveEntrypointFile(entry, [
|
|
100
|
+
...indexedFiles(before),
|
|
101
|
+
...indexedFiles(after),
|
|
102
|
+
]);
|
|
103
|
+
const keys = [
|
|
104
|
+
...new Set([
|
|
105
|
+
...exportsInFile(file, before),
|
|
106
|
+
...exportsInFile(file, after),
|
|
107
|
+
].map((info) => info.key)),
|
|
108
|
+
].sort((a, b) => a.localeCompare(b));
|
|
109
|
+
if (keys.length === 0) {
|
|
110
|
+
throw new Error(`No exported entrypoints in ${file}`);
|
|
111
|
+
}
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
const id = `${file}\0${key}`;
|
|
114
|
+
if (seen.has(id))
|
|
115
|
+
continue;
|
|
116
|
+
seen.add(id);
|
|
117
|
+
const beforeInfo = allFunctions(before).find((fn) => fn.file === file && fn.key === key && fn.exported);
|
|
118
|
+
const afterInfo = allFunctions(after).find((fn) => fn.file === file && fn.key === key && fn.exported);
|
|
119
|
+
out.push({ key, beforeInfo, afterInfo, file });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
export function diffEntry(key, before, after, maxDepth) {
|
|
125
|
+
const beforeKey = resolveEntry(key, before) ?? key;
|
|
126
|
+
const afterKey = resolveEntry(key, after) ?? key;
|
|
127
|
+
const hasBefore = before.has(beforeKey);
|
|
128
|
+
const hasAfter = after.has(afterKey);
|
|
129
|
+
if (!hasBefore && !hasAfter)
|
|
130
|
+
return null;
|
|
131
|
+
const beforeTree = hasBefore
|
|
132
|
+
? buildCallTree(beforeKey, before, maxDepth)
|
|
133
|
+
: emptyCallTree(afterKey, after.get(afterKey)?.label ?? afterKey);
|
|
134
|
+
const afterTree = hasAfter
|
|
135
|
+
? buildCallTree(afterKey, after, maxDepth)
|
|
136
|
+
: emptyCallTree(beforeKey, before.get(beforeKey)?.label ?? beforeKey);
|
|
137
|
+
if (!hasBefore && hasAfter) {
|
|
138
|
+
const diff = diffTrees({ key: afterKey, label: afterTree.label, children: [] }, afterTree);
|
|
139
|
+
return { ...diff, status: "added" };
|
|
140
|
+
}
|
|
141
|
+
if (hasBefore && !hasAfter) {
|
|
142
|
+
const diff = diffTrees(beforeTree, {
|
|
143
|
+
key: beforeKey,
|
|
144
|
+
label: beforeTree.label,
|
|
145
|
+
children: [],
|
|
146
|
+
});
|
|
147
|
+
// The after side is a stub with no definition; the removed root is still a
|
|
148
|
+
// definition in the before snapshot, so keep it for source lookups.
|
|
149
|
+
diff.status = "removed";
|
|
150
|
+
if (beforeTree.definition)
|
|
151
|
+
diff.definition = beforeTree.definition;
|
|
152
|
+
return diff;
|
|
153
|
+
}
|
|
154
|
+
const diff = diffTrees(beforeTree, afterTree);
|
|
155
|
+
if (!treeHasChanges(diff))
|
|
156
|
+
return null;
|
|
157
|
+
return diff;
|
|
158
|
+
}
|
|
159
|
+
/** Diff two file-pinned definitions (or missing on one side). */
|
|
160
|
+
export function diffPinnedEntry(key, beforeInfo, afterInfo, before, after, maxDepth) {
|
|
161
|
+
if (!beforeInfo && !afterInfo)
|
|
162
|
+
return null;
|
|
163
|
+
const beforeTree = beforeInfo
|
|
164
|
+
? buildCallTreeFromInfo(beforeInfo, before, maxDepth)
|
|
165
|
+
: emptyCallTree(key, afterInfo?.label ?? key);
|
|
166
|
+
const afterTree = afterInfo
|
|
167
|
+
? buildCallTreeFromInfo(afterInfo, after, maxDepth)
|
|
168
|
+
: emptyCallTree(key, beforeInfo?.label ?? key);
|
|
169
|
+
if (!beforeInfo && afterInfo) {
|
|
170
|
+
const diff = diffTrees({ key, label: afterTree.label, children: [] }, afterTree);
|
|
171
|
+
return { ...diff, status: "added" };
|
|
172
|
+
}
|
|
173
|
+
if (beforeInfo && !afterInfo) {
|
|
174
|
+
const diff = diffTrees(beforeTree, {
|
|
175
|
+
key,
|
|
176
|
+
label: beforeTree.label,
|
|
177
|
+
children: [],
|
|
178
|
+
});
|
|
179
|
+
// Same as diffEntry: a removed root keeps the definition it had in `before`.
|
|
180
|
+
diff.status = "removed";
|
|
181
|
+
if (beforeTree.definition)
|
|
182
|
+
diff.definition = beforeTree.definition;
|
|
183
|
+
return diff;
|
|
184
|
+
}
|
|
185
|
+
const diff = diffTrees(beforeTree, afterTree);
|
|
186
|
+
if (!treeHasChanges(diff))
|
|
187
|
+
return null;
|
|
188
|
+
return diff;
|
|
189
|
+
}
|