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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
function errnoCode(err) {
|
|
8
|
+
// SAFETY: Node require/fs failures are ErrnoException with optional string code.
|
|
9
|
+
return err.code;
|
|
10
|
+
}
|
|
11
|
+
/** On-disk cache of npm-installed tree-sitter grammar packages. */
|
|
12
|
+
export function grammarCacheDir() {
|
|
13
|
+
const override = process.env.CALLDIFF_GRAMMAR_CACHE;
|
|
14
|
+
if (override)
|
|
15
|
+
return override;
|
|
16
|
+
return join(homedir(), ".cache", "calldiff", "grammars");
|
|
17
|
+
}
|
|
18
|
+
function packageInstalled(cacheDir, npmPackage) {
|
|
19
|
+
return existsSync(join(cacheDir, "node_modules", npmPackage));
|
|
20
|
+
}
|
|
21
|
+
const cacheManifestSchema = z
|
|
22
|
+
.object({ allowScripts: z.record(z.string(), z.unknown()).optional() })
|
|
23
|
+
.catchall(z.unknown());
|
|
24
|
+
/**
|
|
25
|
+
* The cache's own package.json, which also allows the grammar's install
|
|
26
|
+
* script: npm 12 blocks dependency install scripts unless `allowScripts`
|
|
27
|
+
* names them, and rejects `--allow-scripts` in a project install like this
|
|
28
|
+
* one. Earlier npm ignores the field. A cache written before this keeps its
|
|
29
|
+
* other fields and gains the entry.
|
|
30
|
+
*/
|
|
31
|
+
export function ensureCachePackageJson(cacheDir, npmPackage) {
|
|
32
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
33
|
+
const pkgPath = join(cacheDir, "package.json");
|
|
34
|
+
let pkg = {
|
|
35
|
+
name: "calldiff-grammar-cache",
|
|
36
|
+
private: true,
|
|
37
|
+
description: "On-demand tree-sitter grammars for calldiff",
|
|
38
|
+
};
|
|
39
|
+
if (existsSync(pkgPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = cacheManifestSchema.safeParse(JSON.parse(readFileSync(pkgPath, "utf8")));
|
|
42
|
+
if (parsed.success)
|
|
43
|
+
pkg = parsed.data;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// An unreadable cache manifest is replaced; the grammars beside it stay.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (pkg.allowScripts?.[npmPackage] === true && existsSync(pkgPath))
|
|
50
|
+
return;
|
|
51
|
+
pkg.allowScripts = { ...pkg.allowScripts, [npmPackage]: true };
|
|
52
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Absolute path to one of npm's JS entry points (`npm-cli.js`, `npx-cli.js`),
|
|
56
|
+
* or undefined when npm is not installed beside Node.
|
|
57
|
+
*/
|
|
58
|
+
export function npmCliPath(cli, directories = [
|
|
59
|
+
...(process.env.PATH ?? "").split(";"),
|
|
60
|
+
dirname(process.execPath),
|
|
61
|
+
]) {
|
|
62
|
+
for (const raw of directories) {
|
|
63
|
+
const directory = raw.trim().replace(/^"|"$/g, "");
|
|
64
|
+
// Never resolve executables from the repository being reviewed.
|
|
65
|
+
if (!isAbsolute(directory))
|
|
66
|
+
continue;
|
|
67
|
+
const path = join(directory, "node_modules", "npm", "bin", cli);
|
|
68
|
+
if (existsSync(path))
|
|
69
|
+
return path;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* On Windows, npm.cmd is a shell script, not an executable. Run npm's JS entry
|
|
75
|
+
* with Node instead: cache paths (including spaces and percent signs) remain
|
|
76
|
+
* literal argv values rather than being interpreted by cmd.exe.
|
|
77
|
+
*/
|
|
78
|
+
export function npmSpawnSpec(args, platform = process.platform) {
|
|
79
|
+
if (platform !== "win32")
|
|
80
|
+
return { file: "npm", args };
|
|
81
|
+
const cli = npmCliPath("npm-cli.js");
|
|
82
|
+
if (cli === undefined) {
|
|
83
|
+
throw new Error("Cannot locate npm's npm-cli.js. Install Node.js with npm and add its directory to PATH.");
|
|
84
|
+
}
|
|
85
|
+
return { file: process.execPath, args: [cli, ...args] };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Some grammar packages (e.g. tree-sitter-c-sharp ≥0.23.5) ship ESM bindings
|
|
89
|
+
* with top-level await, which `require()` cannot load. Fall back to the native
|
|
90
|
+
* `.node` addon via node-gyp-build — same payload the JS wrapper would return.
|
|
91
|
+
*/
|
|
92
|
+
function loadNativeBinding(packageRoot) {
|
|
93
|
+
try {
|
|
94
|
+
const require = createRequire(join(packageRoot, "package.json"));
|
|
95
|
+
// SAFETY: node-gyp-build's default export is (root) => native binding.
|
|
96
|
+
const gypBuild = require("node-gyp-build");
|
|
97
|
+
const binding = gypBuild(packageRoot);
|
|
98
|
+
try {
|
|
99
|
+
binding.nodeTypeInfo = require(join(packageRoot, "src", "node-types.json"));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// optional metadata
|
|
103
|
+
}
|
|
104
|
+
return binding;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Expected native machine type for this host, for checking prebuild headers.
|
|
112
|
+
* ELF e_machine (Linux), Mach-O cputype (macOS), PE COFF machine (Windows).
|
|
113
|
+
*/
|
|
114
|
+
function expectedMachine() {
|
|
115
|
+
if (process.platform === "linux") {
|
|
116
|
+
return process.arch === "x64" ? 62 : process.arch === "arm64" ? 183 : null;
|
|
117
|
+
}
|
|
118
|
+
if (process.platform === "darwin") {
|
|
119
|
+
return process.arch === "x64"
|
|
120
|
+
? 0x01000007
|
|
121
|
+
: process.arch === "arm64"
|
|
122
|
+
? 0x0100000c
|
|
123
|
+
: null;
|
|
124
|
+
}
|
|
125
|
+
if (process.platform === "win32") {
|
|
126
|
+
return process.arch === "x64" ? 0x8664 : process.arch === "arm64" ? 0xaa64 : null;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/** Machine type declared in a native prebuild's header, or null if unreadable. */
|
|
131
|
+
function prebuildMachine(prebuildPath) {
|
|
132
|
+
let header;
|
|
133
|
+
try {
|
|
134
|
+
header = readFileSync(prebuildPath).subarray(0, 64);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (header.length < 20)
|
|
140
|
+
return null;
|
|
141
|
+
// ELF: 7f 45 4c 46, e_machine is a little-endian u16 at offset 18.
|
|
142
|
+
if (header[0] === 0x7f && header[1] === 0x45 && header[2] === 0x4c && header[3] === 0x46) {
|
|
143
|
+
return header.readUInt16LE(18);
|
|
144
|
+
}
|
|
145
|
+
// Mach-O 64-bit: FE ED FA CF (little-endian), cputype is a u32 at offset 4.
|
|
146
|
+
if (header[0] === 0xcf &&
|
|
147
|
+
header[1] === 0xfa &&
|
|
148
|
+
header[2] === 0xed &&
|
|
149
|
+
header[3] === 0xfe) {
|
|
150
|
+
return header.readUInt32LE(4);
|
|
151
|
+
}
|
|
152
|
+
// PE: 4D 5A ("MZ"), e_lfanew at 0x3C points at the COFF header whose first
|
|
153
|
+
// u16 is the machine type.
|
|
154
|
+
if (header[0] === 0x4d && header[1] === 0x5a) {
|
|
155
|
+
const peOffset = header.readUInt32LE(0x3c);
|
|
156
|
+
if (peOffset + 2 <= header.length)
|
|
157
|
+
return header.readUInt16LE(peOffset);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Path of this platform's prebuild when the file exists but targets another
|
|
163
|
+
* CPU (e.g. x86-64 bytes shipped as linux-arm64). Null when the prebuild is
|
|
164
|
+
* absent, unreadable, or correct: only a confirmed mismatch is reported.
|
|
165
|
+
* Exported for tests and diagnostics.
|
|
166
|
+
*/
|
|
167
|
+
export function mislabeledPrebuild(packageRoot) {
|
|
168
|
+
const expected = expectedMachine();
|
|
169
|
+
if (expected === null)
|
|
170
|
+
return null;
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = readdirSync(join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`), {
|
|
174
|
+
withFileTypes: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (!entry.isFile() || !entry.name.endsWith(".node"))
|
|
182
|
+
continue;
|
|
183
|
+
const full = join(packageRoot, "prebuilds", `${process.platform}-${process.arch}`, entry.name);
|
|
184
|
+
const machine = prebuildMachine(full);
|
|
185
|
+
if (machine !== null && machine !== expected)
|
|
186
|
+
return full;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Explain a native load failure in actionable terms. Extraction treats load
|
|
192
|
+
* failures as per-file warnings, so the hint must name the fix.
|
|
193
|
+
*/
|
|
194
|
+
function withNativeLoadHints(npmPackage, err) {
|
|
195
|
+
if (/GLIBCXX|GLIBC_/i.test(err.message)) {
|
|
196
|
+
return (`${err.message} — the ${npmPackage} native module needs libstdc++ from GCC 13.1 ` +
|
|
197
|
+
`or newer (for example Ubuntu 24.04+). Rebuild it for this host with ` +
|
|
198
|
+
`\`npm rebuild ${npmPackage} --build-from-source\`, or upgrade libstdc++.`);
|
|
199
|
+
}
|
|
200
|
+
return err.message;
|
|
201
|
+
}
|
|
202
|
+
function requireGrammar(require, npmPackage, packageRoot) {
|
|
203
|
+
try {
|
|
204
|
+
// SAFETY: grammar packages export a module compatible with GrammarModule.
|
|
205
|
+
return require(npmPackage);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// SAFETY: catch bindings are unknown; normalize to Error at the boundary.
|
|
209
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
210
|
+
const code = errnoCode(failure);
|
|
211
|
+
if (code === "ERR_REQUIRE_ASYNC_MODULE" ||
|
|
212
|
+
failure.message.includes("top-level await")) {
|
|
213
|
+
const binding = loadNativeBinding(packageRoot);
|
|
214
|
+
if (binding)
|
|
215
|
+
return binding;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// A mislabeled prebuild blocks node-gyp-build's own source-build
|
|
219
|
+
// fallback. This directory is diffninja's own grammar cache, so remove
|
|
220
|
+
// the bad artifact and let the load retry compile from source.
|
|
221
|
+
const bad = mislabeledPrebuild(packageRoot);
|
|
222
|
+
if (bad !== null) {
|
|
223
|
+
rmSync(bad, { force: true });
|
|
224
|
+
try {
|
|
225
|
+
// SAFETY: grammar packages export a module compatible with GrammarModule.
|
|
226
|
+
return require(npmPackage);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
const binding = loadNativeBinding(packageRoot);
|
|
230
|
+
if (binding)
|
|
231
|
+
return binding;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
throw new Error(withNativeLoadHints(npmPackage, failure));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function installSpecFor(npmPackage) {
|
|
239
|
+
switch (npmPackage) {
|
|
240
|
+
case "tree-sitter-c-sharp":
|
|
241
|
+
return "tree-sitter-c-sharp@0.23.1";
|
|
242
|
+
case "@tree-sitter-grammars/tree-sitter-lua":
|
|
243
|
+
// 0.4+ is ESM-with-TLA; 0.2.0 is CJS and loads via createRequire.
|
|
244
|
+
return "@tree-sitter-grammars/tree-sitter-lua@0.2.0";
|
|
245
|
+
default:
|
|
246
|
+
return npmPackage;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function sleepMs(ms) {
|
|
250
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
251
|
+
}
|
|
252
|
+
/** Why this grammar's install can fail beyond npm itself, for the error text. */
|
|
253
|
+
function installFailureHint(npmPackage) {
|
|
254
|
+
if (npmPackage === "tree-sitter-swift") {
|
|
255
|
+
return ("tree-sitter-swift downloads its parser CLI from GitHub Releases during " +
|
|
256
|
+
"install; a proxy or offline network fails that download. Retry with " +
|
|
257
|
+
"network access to github.com, or pre-populate CALLDIFF_GRAMMAR_CACHE.");
|
|
258
|
+
}
|
|
259
|
+
if (npmPackage === "tree-sitter-perl" || npmPackage === "tree-sitter-kotlin") {
|
|
260
|
+
return ("this grammar ships no prebuilt binary and compiles from source: Python " +
|
|
261
|
+
"and a C/C++ toolchain (build-essential, Xcode command line tools, or " +
|
|
262
|
+
"Visual Studio Build Tools) are required.");
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Install an on-demand grammar into the cache, retrying transient network
|
|
268
|
+
* failures (the Swift parser CLI download is the known flaky one). Throws an
|
|
269
|
+
* actionable error when the install cannot succeed.
|
|
270
|
+
*/
|
|
271
|
+
function installGrammarPackage(cacheDir, npmPackage) {
|
|
272
|
+
const npm = npmSpawnSpec([
|
|
273
|
+
"install",
|
|
274
|
+
"--prefix",
|
|
275
|
+
cacheDir,
|
|
276
|
+
"--no-save",
|
|
277
|
+
"--no-fund",
|
|
278
|
+
"--no-audit",
|
|
279
|
+
"--legacy-peer-deps",
|
|
280
|
+
installSpecFor(npmPackage),
|
|
281
|
+
]);
|
|
282
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
283
|
+
try {
|
|
284
|
+
execFileSync(npm.file, npm.args, {
|
|
285
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
286
|
+
env: process.env,
|
|
287
|
+
timeout: 300_000,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
if (attempt === 3) {
|
|
293
|
+
// SAFETY: catch bindings are unknown; normalize to Error at the boundary.
|
|
294
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
295
|
+
const hint = installFailureHint(npmPackage);
|
|
296
|
+
throw new Error(`Could not install the ${npmPackage} grammar into ${cacheDir}: ${failure.message}` +
|
|
297
|
+
(hint ? ` ${hint}` : ""));
|
|
298
|
+
}
|
|
299
|
+
sleepMs(2000 * attempt);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Install an npm grammar package into the shared cache if missing, then require it.
|
|
305
|
+
* Reuses the cache across CLI invocations.
|
|
306
|
+
*/
|
|
307
|
+
export function loadGrammarPackage(npmPackage) {
|
|
308
|
+
// Prefer the app's own dependency when present (e.g. tree-sitter-typescript).
|
|
309
|
+
try {
|
|
310
|
+
const localRequire = createRequire(import.meta.url);
|
|
311
|
+
try {
|
|
312
|
+
// SAFETY: local dependency resolves to a tree-sitter grammar module.
|
|
313
|
+
return localRequire(npmPackage);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
// SAFETY: catch bindings are unknown; normalize to Error at the boundary.
|
|
317
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
318
|
+
const code = errnoCode(failure);
|
|
319
|
+
if (code === "ERR_REQUIRE_ASYNC_MODULE" ||
|
|
320
|
+
failure.message.includes("top-level await")) {
|
|
321
|
+
const entry = localRequire.resolve(npmPackage);
|
|
322
|
+
const packageRoot = join(entry, "..", "..");
|
|
323
|
+
const binding = loadNativeBinding(packageRoot);
|
|
324
|
+
if (binding)
|
|
325
|
+
return binding;
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// fall through to cache
|
|
332
|
+
}
|
|
333
|
+
const cacheDir = grammarCacheDir();
|
|
334
|
+
if (!packageInstalled(cacheDir, npmPackage)) {
|
|
335
|
+
ensureCachePackageJson(cacheDir, npmPackage);
|
|
336
|
+
installGrammarPackage(cacheDir, npmPackage);
|
|
337
|
+
}
|
|
338
|
+
const require = createRequire(join(cacheDir, "package.json"));
|
|
339
|
+
const packageRoot = join(cacheDir, "node_modules", npmPackage);
|
|
340
|
+
return requireGrammar(require, npmPackage, packageRoot);
|
|
341
|
+
}
|
|
342
|
+
/** Resolve the value to pass to parser.setLanguage. */
|
|
343
|
+
export function resolveLanguage(mod, exportName) {
|
|
344
|
+
if (exportName) {
|
|
345
|
+
const named = mod[exportName];
|
|
346
|
+
if (named != null)
|
|
347
|
+
return named;
|
|
348
|
+
}
|
|
349
|
+
// Native grammar packages export { language, nodeTypeInfo, ... } — pass the module.
|
|
350
|
+
return mod;
|
|
351
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { heuristicCallSyntax } from "./call-syntax.js";
|
|
2
|
+
import { childByType, collapseWs, locFromNode, namedChildren, } from "./types.js";
|
|
3
|
+
function isLikelyTypeName(name) {
|
|
4
|
+
const c = name[0];
|
|
5
|
+
return c !== undefined && c === c.toUpperCase() && c !== c.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function patternNames(patterns) {
|
|
8
|
+
if (!patterns)
|
|
9
|
+
return [];
|
|
10
|
+
const names = [];
|
|
11
|
+
for (const p of namedChildren(patterns)) {
|
|
12
|
+
if (p.type === "variable")
|
|
13
|
+
names.push(p.text);
|
|
14
|
+
else if (p.type === "wildcard")
|
|
15
|
+
names.push("_");
|
|
16
|
+
else
|
|
17
|
+
names.push("_");
|
|
18
|
+
}
|
|
19
|
+
return names;
|
|
20
|
+
}
|
|
21
|
+
function getParamsLabel(patterns) {
|
|
22
|
+
const names = patternNames(patterns);
|
|
23
|
+
return names.length === 0 ? "()" : `(${names.join(", ")})`;
|
|
24
|
+
}
|
|
25
|
+
function calleeFromExp(node) {
|
|
26
|
+
if (node.type === "variable") {
|
|
27
|
+
if (node.text === "return" || node.text === "pure")
|
|
28
|
+
return null;
|
|
29
|
+
return node.text;
|
|
30
|
+
}
|
|
31
|
+
if (node.type === "apply") {
|
|
32
|
+
// f x y → callee is leftmost function
|
|
33
|
+
const head = node.namedChild(0);
|
|
34
|
+
if (!head)
|
|
35
|
+
return null;
|
|
36
|
+
return calleeFromExp(head);
|
|
37
|
+
}
|
|
38
|
+
if (node.type === "parens") {
|
|
39
|
+
const inner = node.namedChild(0);
|
|
40
|
+
return inner ? calleeFromExp(inner) : null;
|
|
41
|
+
}
|
|
42
|
+
if (node.type === "qualified") {
|
|
43
|
+
// M.f
|
|
44
|
+
const kids = namedChildren(node);
|
|
45
|
+
const mod = kids.find((c) => c.type === "module" || c.type === "module_id");
|
|
46
|
+
const vari = kids.find((c) => c.type === "variable");
|
|
47
|
+
if (vari && mod) {
|
|
48
|
+
const modName = mod.type === "module"
|
|
49
|
+
? (childByType(mod, "module_id")?.text ?? mod.text.replace(/\.$/, ""))
|
|
50
|
+
: mod.text.replace(/\.$/, "");
|
|
51
|
+
return `${modName}.${vari.text}`;
|
|
52
|
+
}
|
|
53
|
+
if (vari)
|
|
54
|
+
return vari.text;
|
|
55
|
+
}
|
|
56
|
+
// constructors / operators / literals — ignore as callees when not useful
|
|
57
|
+
if (node.type === "constructor" || node.type === "operator")
|
|
58
|
+
return null;
|
|
59
|
+
if (node.type === "literal" || node.type === "unit")
|
|
60
|
+
return null;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function collectFromMatch(file, match) {
|
|
64
|
+
if (!match)
|
|
65
|
+
return [];
|
|
66
|
+
const body = match.namedChild(0) ?? namedChildren(match)[0] ?? null;
|
|
67
|
+
if (!body)
|
|
68
|
+
return [];
|
|
69
|
+
return collectExpr(file, body);
|
|
70
|
+
}
|
|
71
|
+
function collectExpr(file, node) {
|
|
72
|
+
const steps = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const addCall = (key, node) => {
|
|
75
|
+
const mark = `${key}:${node.startIndex}`;
|
|
76
|
+
if (seen.has(mark))
|
|
77
|
+
return;
|
|
78
|
+
seen.add(mark);
|
|
79
|
+
const step = { type: "call", key, ...locFromNode(file, node) };
|
|
80
|
+
step.syntax = heuristicCallSyntax(node, key);
|
|
81
|
+
steps.push(step);
|
|
82
|
+
};
|
|
83
|
+
const walk = (n) => {
|
|
84
|
+
// Nested binds / lambdas / local functions
|
|
85
|
+
if (n.type === "function" ||
|
|
86
|
+
n.type === "bind" ||
|
|
87
|
+
n.type === "lambda" ||
|
|
88
|
+
n.type === "local_binds" ||
|
|
89
|
+
n.type === "declarations") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (n.type === "conditional") {
|
|
93
|
+
const kids = namedChildren(n);
|
|
94
|
+
const cond = kids[0] ?? null;
|
|
95
|
+
const thenE = kids[1] ?? null;
|
|
96
|
+
const elseE = kids[2] ?? null;
|
|
97
|
+
const condText = cond ? collapseWs(cond.text) : "";
|
|
98
|
+
steps.push({
|
|
99
|
+
type: "branch",
|
|
100
|
+
key: condText ? `if:${condText}` : "if",
|
|
101
|
+
label: condText ? `if ${condText}` : "if",
|
|
102
|
+
...locFromNode(file, cond ?? n),
|
|
103
|
+
children: thenE ? collectExpr(file, thenE) : [],
|
|
104
|
+
});
|
|
105
|
+
if (elseE) {
|
|
106
|
+
steps.push({
|
|
107
|
+
type: "branch",
|
|
108
|
+
key: "else",
|
|
109
|
+
label: "else",
|
|
110
|
+
...locFromNode(file, elseE),
|
|
111
|
+
children: collectExpr(file, elseE),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (n.type === "case") {
|
|
117
|
+
const alts = childByType(n, "alternatives");
|
|
118
|
+
for (const alt of alts ? namedChildren(alts) : []) {
|
|
119
|
+
if (alt.type !== "alternative")
|
|
120
|
+
continue;
|
|
121
|
+
const pattern = namedChildren(alt).find((c) => c.type !== "match") ?? null;
|
|
122
|
+
const text = pattern ? collapseWs(pattern.text) : "";
|
|
123
|
+
const match = childByType(alt, "match");
|
|
124
|
+
steps.push({
|
|
125
|
+
type: "branch",
|
|
126
|
+
key: text ? `case:${text}` : "case",
|
|
127
|
+
label: text ? `case ${text}` : "case",
|
|
128
|
+
...locFromNode(file, pattern ?? alt),
|
|
129
|
+
children: collectFromMatch(file, match),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (n.type === "apply") {
|
|
135
|
+
const key = calleeFromExp(n);
|
|
136
|
+
// Skip type-like constructors: Just x, Runner
|
|
137
|
+
if (key && !isLikelyTypeName(key.split(".").pop() ?? key)) {
|
|
138
|
+
addCall(key, n);
|
|
139
|
+
}
|
|
140
|
+
// Walk argument expressions for nested applies, but not bare variable args
|
|
141
|
+
for (const child of namedChildren(n).slice(1)) {
|
|
142
|
+
if (child.type === "variable" || child.type === "literal" || child.type === "unit") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (child.type === "parens") {
|
|
146
|
+
const inner = child.namedChild(0);
|
|
147
|
+
if (inner)
|
|
148
|
+
walk(inner);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
walk(child);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (n.type === "variable") {
|
|
156
|
+
// Bare variable used as expression in do-block is a call-ish nullary
|
|
157
|
+
if (n.text !== "return" && n.text !== "pure" && !isLikelyTypeName(n.text)) {
|
|
158
|
+
addCall(n.text, n);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (n.type === "qualified") {
|
|
163
|
+
const key = calleeFromExp(n);
|
|
164
|
+
if (key && !isLikelyTypeName(key.split(".").pop() ?? key)) {
|
|
165
|
+
addCall(key, n);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (n.type === "infix") {
|
|
170
|
+
for (const child of namedChildren(n)) {
|
|
171
|
+
if (child.type === "operator")
|
|
172
|
+
continue;
|
|
173
|
+
walk(child);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (n.type === "do") {
|
|
178
|
+
for (const child of namedChildren(n)) {
|
|
179
|
+
if (child.type === "exp") {
|
|
180
|
+
const inner = child.namedChild(0) ?? child;
|
|
181
|
+
walk(inner);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
walk(child);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Conditionals already handled; avoid double-counting by not
|
|
190
|
+
// falling through into then/else when somehow re-entered.
|
|
191
|
+
for (const child of namedChildren(n))
|
|
192
|
+
walk(child);
|
|
193
|
+
};
|
|
194
|
+
walk(node);
|
|
195
|
+
return steps;
|
|
196
|
+
}
|
|
197
|
+
function handleFunctionOrBind(file, node, functions) {
|
|
198
|
+
const name = childByType(node, "variable")?.text ??
|
|
199
|
+
namedChildren(node).find((c) => c.type === "variable")?.text ??
|
|
200
|
+
null;
|
|
201
|
+
if (!name)
|
|
202
|
+
return;
|
|
203
|
+
// Skip type signatures mistakenly matched — signatures use `signature` type
|
|
204
|
+
const patterns = childByType(node, "patterns");
|
|
205
|
+
const match = childByType(node, "match");
|
|
206
|
+
functions.push({
|
|
207
|
+
key: name,
|
|
208
|
+
label: `${name}${getParamsLabel(patterns)}`,
|
|
209
|
+
file,
|
|
210
|
+
steps: collectFromMatch(file, match),
|
|
211
|
+
exported: true,
|
|
212
|
+
start: node.startIndex,
|
|
213
|
+
end: node.endIndex,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function visitDecls(file, decls, functions) {
|
|
217
|
+
for (const d of namedChildren(decls)) {
|
|
218
|
+
if (d.type === "function" || d.type === "bind") {
|
|
219
|
+
// `function` nodes also appear inside signatures — only top-level with match
|
|
220
|
+
if (childByType(d, "match") || d.type === "bind") {
|
|
221
|
+
handleFunctionOrBind(file, d, functions);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function extractFromTree(file, _source, tree) {
|
|
227
|
+
const functions = [];
|
|
228
|
+
const root = tree.rootNode;
|
|
229
|
+
// haskell → declarations, or header + declarations
|
|
230
|
+
for (const child of namedChildren(root)) {
|
|
231
|
+
if (child.type === "declarations") {
|
|
232
|
+
visitDecls(file, child, functions);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Some files may root directly under declarations-less structure
|
|
236
|
+
if (functions.length === 0) {
|
|
237
|
+
for (const child of namedChildren(root)) {
|
|
238
|
+
if (child.type === "function" || child.type === "bind") {
|
|
239
|
+
handleFunctionOrBind(file, child, functions);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return functions;
|
|
244
|
+
}
|
|
245
|
+
export const haskellExtractor = {
|
|
246
|
+
id: "haskell",
|
|
247
|
+
extensions: [".hs"],
|
|
248
|
+
grammarPackage: "tree-sitter-haskell",
|
|
249
|
+
extract: extractFromTree,
|
|
250
|
+
};
|