@azure-id/orc 1.7.1 → 1.8.1
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/CHANGELOG.md +3649 -3381
- package/README-id.md +923 -844
- package/README.md +836 -788
- package/bin/build-agents.js +43 -27
- package/bin/cli.js +701 -3
- package/bin/graph-extract.js +927 -0
- package/bin/graph-notes.js +188 -0
- package/bin/graph-query.js +808 -0
- package/bin/graph-resolve.js +178 -0
- package/bin/graph-signals.js +277 -0
- package/bin/graph.js +605 -0
- package/bin/verify-contracts.js +4669 -4553
- package/bin/verify-package.js +626 -616
- package/bin/webui/api.js +1419 -1414
- package/bin/webui/fixtures/index.js +579 -576
- package/bin/webui/fixtures/knowledge.js +316 -291
- package/bin/webui/i18n/en/knowledge.json +167 -151
- package/bin/webui/i18n/en/overview.json +101 -100
- package/bin/webui/i18n/id/knowledge.json +167 -151
- package/bin/webui/i18n/id/overview.json +101 -100
- package/bin/webui/js/panels/knowledge.js +1065 -1006
- package/bin/webui/js/panels/overview.js +492 -488
- package/package.json +39 -39
- package/templates/agents/MODEL-MAPPING.md +163 -158
- package/templates/agents/orc-executor-haiku-4-5.md +133 -121
- package/templates/agents/orc-executor-opus-4-7-high.md +134 -122
- package/templates/agents/orc-executor-opus-4-7-med.md +134 -122
- package/templates/agents/orc-executor-opus-4-8-high.md +134 -122
- package/templates/agents/orc-executor-opus-5-high.md +134 -122
- package/templates/agents/orc-executor-opus-5-low.md +134 -122
- package/templates/agents/orc-executor-opus-5-med.md +134 -122
- package/templates/agents/orc-executor-sonnet-4-6-high.md +134 -122
- package/templates/agents/orc-executor-sonnet-4-6-med.md +134 -122
- package/templates/agents/orc-executor-sonnet-5-high.md +134 -122
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +86 -0
- package/templates/hooks/README.md +444 -396
- package/templates/hooks/orc-graph-hook.js +336 -0
- package/templates/hooks/orc-statusline-render.js +922 -921
- package/templates/hooks/orc-statusline.js +1596 -1545
- package/templates/skills/_shared/README.md +4 -0
- package/templates/skills/_shared/code-graph.md +220 -0
- package/templates/skills/_shared/opus5-only.md +4 -0
- package/templates/skills/_shared/phases/execution.md +166 -147
- package/templates/skills/_shared/phases/planning.md +142 -135
- package/templates/skills/_shared/phases/preflight.md +132 -118
- package/templates/skills/_shared/phases/review.md +63 -53
- package/templates/skills/_shared/phases/ship.md +96 -88
- package/templates/skills/_shared/phases/trace.md +6 -0
- package/templates/skills/_shared/phases/wiki-consult.md +194 -189
- package/templates/skills/_shared/read-ladder.md +124 -102
- package/templates/skills/_shared/return-validation.md +259 -250
- package/templates/skills/orc/SKILL.md +255 -254
- package/templates/skills/orc-diy/references/flow-schema.md +101 -100
- package/templates/skills/orc-fast/SKILL.md +236 -229
- package/templates/skills/orc-mini/SKILL.md +267 -259
- package/templates/skills/orc-quick/SKILL.md +378 -361
- package/templates/skills/orc-quick/references/dispatch-gate.md +6 -0
- package/templates/skills/orc-wiki/references/staleness.md +294 -288
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── orc graph — EXTRACTION (v1.8.0 W2) ─────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// One source file in, one record out: imports, symbols (with line ranges and a
|
|
5
|
+
// body hash), the calls each symbol makes, and the side effects it has (SQL,
|
|
6
|
+
// db, HTTP, env, fs). No model, no dependency.
|
|
7
|
+
//
|
|
8
|
+
// THE LADDER (D3)
|
|
9
|
+
//
|
|
10
|
+
// 1. BORROWED TOOLCHAIN — Python's own `ast` module, when a Python ≥3.8 runs
|
|
11
|
+
// on this machine. Exact symbols, ranges, calls and imports. One
|
|
12
|
+
// subprocess for the whole batch.
|
|
13
|
+
// 2. HEURISTIC — a comment/string MASK first (so a call inside a comment or a
|
|
14
|
+
// string is never a call), then per-language declaration patterns, brace
|
|
15
|
+
// or indentation matching for the body, and a call scan.
|
|
16
|
+
//
|
|
17
|
+
// TypeScript's own parser is NOT borrowed in this release: no TypeScript was
|
|
18
|
+
// available to test it against, and an untested extractor is worse than an
|
|
19
|
+
// honest heuristic. `ORC_GRAPH_NO_BORROW=1` forces rung 2 (tests use it).
|
|
20
|
+
//
|
|
21
|
+
// What the record promises, and what it does not:
|
|
22
|
+
// - `body_hash` changes when THIS symbol's text changes, and not when another
|
|
23
|
+
// symbol in the same file changes. Notes (W4) key on it.
|
|
24
|
+
// - calls are NAMES, not targets. Resolution is `graph-query.js`'s job, and it
|
|
25
|
+
// is computed on read.
|
|
26
|
+
// - dynamic dispatch, reflection, macros and eval are invisible. The graph is
|
|
27
|
+
// a LOCATOR; the code is still the truth.
|
|
28
|
+
|
|
29
|
+
const crypto = require("crypto");
|
|
30
|
+
const { spawnSync } = require("child_process");
|
|
31
|
+
|
|
32
|
+
// @3 (W9): route handlers are symbols, and a function passed BY NAME is a `ref`.
|
|
33
|
+
// @4 (EW1): every record says how much of the file the extractor actually saw.
|
|
34
|
+
const HEURISTIC = "heuristic@4";
|
|
35
|
+
const MAX_CALLS_PER_SYMBOL = 200;
|
|
36
|
+
const MAX_EFFECTS_PER_SYMBOL = 12;
|
|
37
|
+
|
|
38
|
+
const KW = new Set(
|
|
39
|
+
(
|
|
40
|
+
"if for while switch catch function return typeof sizeof def class elif with not and or in " +
|
|
41
|
+
"await yield case new throw else do try foreach using lock fixed func go defer select range " +
|
|
42
|
+
"match fn when elseif isset empty unset array list print echo import export from lambda assert " +
|
|
43
|
+
"del pass raise except finally is this self super base var let const delete instanceof " +
|
|
44
|
+
"require_once include_once require include die exit"
|
|
45
|
+
).split(" ")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// ── text utilities ──────────────────────────────────────────────────────────
|
|
49
|
+
function lineIndex(src) {
|
|
50
|
+
const starts = [0];
|
|
51
|
+
for (let i = 0; i < src.length; i++) if (src.charCodeAt(i) === 10) starts.push(i + 1);
|
|
52
|
+
return starts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lineOf(starts, pos) {
|
|
56
|
+
let lo = 0;
|
|
57
|
+
let hi = starts.length - 1;
|
|
58
|
+
while (lo < hi) {
|
|
59
|
+
const mid = (lo + hi + 1) >> 1;
|
|
60
|
+
if (starts[mid] <= pos) lo = mid;
|
|
61
|
+
else hi = mid - 1;
|
|
62
|
+
}
|
|
63
|
+
return lo + 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const clip = (s, n = 80) => {
|
|
67
|
+
const t = String(s).replace(/\s+/g, " ").trim();
|
|
68
|
+
return t.length > n ? t.slice(0, n - 1) + "…" : t;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const hash = (s) => crypto.createHash("sha1").update(String(s).replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
|
|
72
|
+
|
|
73
|
+
// Replace comment and string CONTENT with spaces, keeping every newline and
|
|
74
|
+
// every offset. Declarations and calls are found in the mask; string text (for
|
|
75
|
+
// SQL and URLs) is read back from the original at the recorded spans.
|
|
76
|
+
function mask(src, lang) {
|
|
77
|
+
const slash = lang !== "py";
|
|
78
|
+
const hashC = lang === "py" || lang === "php";
|
|
79
|
+
const block = lang !== "py";
|
|
80
|
+
const backtick = lang === "js" || lang === "ts" || lang === "go";
|
|
81
|
+
const m = src.split("");
|
|
82
|
+
const n = src.length;
|
|
83
|
+
const strings = [];
|
|
84
|
+
const blank = (a, b) => {
|
|
85
|
+
for (let k = a; k < b && k < n; k++) if (m[k] !== "\n" && m[k] !== "\r") m[k] = " ";
|
|
86
|
+
};
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < n) {
|
|
89
|
+
const c = src[i];
|
|
90
|
+
const c2 = src[i + 1];
|
|
91
|
+
if ((slash && c === "/" && c2 === "/") || (hashC && c === "#" && !(lang === "php" && c2 === "["))) {
|
|
92
|
+
const e = src.indexOf("\n", i);
|
|
93
|
+
const end = e < 0 ? n : e;
|
|
94
|
+
blank(i, end);
|
|
95
|
+
i = end;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (block && c === "/" && c2 === "*") {
|
|
99
|
+
const e = src.indexOf("*/", i + 2);
|
|
100
|
+
const end = e < 0 ? n : e + 2;
|
|
101
|
+
blank(i, end);
|
|
102
|
+
i = end;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (lang === "py" && (src.startsWith('"""', i) || src.startsWith("'''", i))) {
|
|
106
|
+
const q = src.substr(i, 3);
|
|
107
|
+
const e = src.indexOf(q, i + 3);
|
|
108
|
+
const end = e < 0 ? n : e;
|
|
109
|
+
strings.push({ start: i + 3, end });
|
|
110
|
+
blank(i + 3, end);
|
|
111
|
+
i = e < 0 ? n : e + 3;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (c === '"' || c === "'" || (backtick && c === "`")) {
|
|
115
|
+
const verbatim = lang === "cs" && src[i - 1] === "@";
|
|
116
|
+
const raw = c === "`" && lang === "go";
|
|
117
|
+
let j = i + 1;
|
|
118
|
+
while (j < n) {
|
|
119
|
+
const d = src[j];
|
|
120
|
+
if (d === "\\" && !verbatim && !raw) {
|
|
121
|
+
j += 2;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (d === c) {
|
|
125
|
+
if (verbatim && src[j + 1] === '"') {
|
|
126
|
+
j += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
// A quote that never closes on its line is not a string (a regex
|
|
132
|
+
// literal, an apostrophe in a template): stop at the line end so one
|
|
133
|
+
// bad guess costs one line, not the rest of the file.
|
|
134
|
+
if (d === "\n" && c !== "`" && !verbatim) break;
|
|
135
|
+
j++;
|
|
136
|
+
}
|
|
137
|
+
strings.push({ start: i + 1, end: Math.min(j, n) });
|
|
138
|
+
blank(i + 1, j);
|
|
139
|
+
i = j + 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
return { masked: m.join(""), strings };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// COVERAGE (EW1). A heuristic extractor can lose the thread: an opening brace
|
|
148
|
+
// with no partner makes a declaration run to the end of the file, and a symbol
|
|
149
|
+
// that hits a cap keeps only part of what it found. Neither is an error — the
|
|
150
|
+
// record is still useful — but the reader must be told which lines are only
|
|
151
|
+
// partly seen, or a card that shows nothing reads as "there is nothing".
|
|
152
|
+
// One extraction runs at a time, so a module-level note is enough.
|
|
153
|
+
let UNBALANCED = -1;
|
|
154
|
+
function resetCoverage() {
|
|
155
|
+
UNBALANCED = -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function matchPair(s, open, a, b) {
|
|
159
|
+
let d = 0;
|
|
160
|
+
for (let k = open; k < s.length; k++) {
|
|
161
|
+
const ch = s[k];
|
|
162
|
+
if (ch === a) d++;
|
|
163
|
+
else if (ch === b) {
|
|
164
|
+
d--;
|
|
165
|
+
if (d === 0) return k;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Only a BLOCK that really never closes is a coverage gap. Two other callers
|
|
169
|
+
// land here and neither is one: a parameter list scanned speculatively, and a
|
|
170
|
+
// probe whose start is not the opener at all (`s[open] !== a`), which can
|
|
171
|
+
// never balance. Noting those marked 40% of this repo's own files partial —
|
|
172
|
+
// a signal that fires everywhere says nothing.
|
|
173
|
+
if (a === "{" && s[open] === a && (UNBALANCED < 0 || open < UNBALANCED)) UNBALANCED = open;
|
|
174
|
+
return s.length - 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// After a parameter list: find the body. `requireArrow` = this is only a
|
|
178
|
+
// function if an `=>` follows (a `const x = (a + b)` is not one).
|
|
179
|
+
function bodyAfter(m, from, requireArrow) {
|
|
180
|
+
const limit = Math.min(m.length, from + 400);
|
|
181
|
+
for (let k = from; k < limit; k++) {
|
|
182
|
+
const ch = m[k];
|
|
183
|
+
if (ch === "=" && m[k + 1] === ">") {
|
|
184
|
+
let j = k + 2;
|
|
185
|
+
while (j < m.length && /\s/.test(m[j])) j++;
|
|
186
|
+
if (m[j] === "{") return { open: j, end: matchPair(m, j, "{", "}") };
|
|
187
|
+
const nl = m.indexOf("\n", j);
|
|
188
|
+
return { open: j, end: nl < 0 ? m.length - 1 : nl - 1 };
|
|
189
|
+
}
|
|
190
|
+
if (ch === "{") return requireArrow ? null : { open: k, end: matchPair(m, k, "{", "}") };
|
|
191
|
+
if (ch === ";") return requireArrow ? null : { open: k, end: k };
|
|
192
|
+
if (ch === "\n" && requireArrow) {
|
|
193
|
+
let j = k + 1;
|
|
194
|
+
while (j < limit && (m[j] === " " || m[j] === "\t" || m[j] === "\r")) j++;
|
|
195
|
+
if (m[j] !== "=" && m[j] !== ":") return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── declarations: brace languages ───────────────────────────────────────────
|
|
202
|
+
function braceDefs(src, m, starts, lang, strings) {
|
|
203
|
+
const defs = [];
|
|
204
|
+
const defParens = new Set();
|
|
205
|
+
const classes = [];
|
|
206
|
+
const add = (d) => {
|
|
207
|
+
if (!d.name || KW.has(d.name)) return;
|
|
208
|
+
d.s = lineOf(starts, d.from);
|
|
209
|
+
d.e = lineOf(starts, Math.max(d.from, d.to));
|
|
210
|
+
defs.push(d);
|
|
211
|
+
};
|
|
212
|
+
const innermostClass = (pos) => {
|
|
213
|
+
let best = null;
|
|
214
|
+
for (const c of classes) if (c.open < pos && pos < c.to && (!best || c.to - c.open < best.to - best.open)) best = c;
|
|
215
|
+
return best;
|
|
216
|
+
};
|
|
217
|
+
let x;
|
|
218
|
+
|
|
219
|
+
if (lang === "js" || lang === "ts") {
|
|
220
|
+
const CLASS = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:abstract[ \t]+)?class[ \t]+([A-Za-z_$][\w$]*)[^{;]*\{/gm;
|
|
221
|
+
while ((x = CLASS.exec(m))) {
|
|
222
|
+
const open = x.index + x[0].length - 1;
|
|
223
|
+
const to = matchPair(m, open, "{", "}");
|
|
224
|
+
const c = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: !!x[1] };
|
|
225
|
+
classes.push(c);
|
|
226
|
+
add(c);
|
|
227
|
+
}
|
|
228
|
+
const FN = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:async[ \t]+)?function[ \t]*\*?[ \t]*([A-Za-z_$][\w$]*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
|
|
229
|
+
while ((x = FN.exec(m))) {
|
|
230
|
+
const paren = x.index + x[0].length - 1;
|
|
231
|
+
const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
|
|
232
|
+
defParens.add(paren);
|
|
233
|
+
// `function f(a: A): B;` is an overload SIGNATURE — no body, not a symbol.
|
|
234
|
+
if (!body || m[body.open] === ";") continue;
|
|
235
|
+
add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
|
|
236
|
+
}
|
|
237
|
+
const ARROW = /^[ \t]*(export[ \t]+)?(?:const|let|var)[ \t]+([A-Za-z_$][\w$]*)[ \t]*(?::[^=\n]+)?=[ \t]*(?:async[ \t]+)?(function\b[^(\n]*\(|\(|[A-Za-z_$][\w$]*[ \t]*=>)/gm;
|
|
238
|
+
while ((x = ARROW.exec(m))) {
|
|
239
|
+
const end = x.index + x[0].length;
|
|
240
|
+
let body;
|
|
241
|
+
if (x[3].endsWith("(")) {
|
|
242
|
+
const paren = end - 1;
|
|
243
|
+
body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, !x[3].startsWith("function"));
|
|
244
|
+
if (body) defParens.add(paren);
|
|
245
|
+
} else {
|
|
246
|
+
body = bodyAfter(m, m.lastIndexOf("=>", end), true);
|
|
247
|
+
}
|
|
248
|
+
if (!body) continue;
|
|
249
|
+
add({ name: x[2], qname: x[2], kind: "function", from: x.index, to: body.end, exported: !!x[1] });
|
|
250
|
+
}
|
|
251
|
+
// Route handlers (Express, Koa, Fastify, Hono): `router.get("/path", mw,
|
|
252
|
+
// (req, res) => { … })`. The handler is anonymous, so without this a route
|
|
253
|
+
// file has NO symbol and its card is an import list — W9 measured 6 files,
|
|
254
|
+
// 1 symbol on an Express app. Named `<METHOD> <path>`; the body is the whole
|
|
255
|
+
// registration call, so the middleware passed to it and every call inside
|
|
256
|
+
// the handler belong to the route.
|
|
257
|
+
const ROUTE = /\b[A-Za-z_$][\w$]*[ \t]*\.[ \t]*(get|post|put|patch|delete|del|all|options|head)[ \t]*\(/g;
|
|
258
|
+
while ((x = ROUTE.exec(m))) {
|
|
259
|
+
const paren = x.index + x[0].length - 1;
|
|
260
|
+
let k = paren + 1;
|
|
261
|
+
while (k < m.length && /\s/.test(m[k])) k++;
|
|
262
|
+
const lit = (strings || []).find((t) => t.start === k + 1);
|
|
263
|
+
if (!lit) continue;
|
|
264
|
+
const routePath = src.slice(lit.start, lit.end);
|
|
265
|
+
if (!/^(\/|\*$)/.test(routePath) || /[\r\n]/.test(routePath)) continue;
|
|
266
|
+
const close = matchPair(m, paren, "(", ")");
|
|
267
|
+
// `cache.get("/k")` and `request(app).get("/orders")` pass no handler.
|
|
268
|
+
if (!/=>|\bfunction\b/.test(m.slice(lit.end + 1, close))) continue;
|
|
269
|
+
const name = `${x[1] === "del" ? "DELETE" : x[1].toUpperCase()} ${clip(routePath, 60)}`;
|
|
270
|
+
add({ name, qname: name, kind: "route", from: x.index, to: close, exported: false });
|
|
271
|
+
}
|
|
272
|
+
// Members: scanned inside each class body, skipping every member body it
|
|
273
|
+
// finds, so a statement inside a method is never read as a member.
|
|
274
|
+
const MEMBER = /^[ \t]*((?:(?:public|private|protected|static|async|readonly|override|abstract|get|set|declare|accessor)[ \t]+)*)\*?[ \t]*(#?[A-Za-z_$][\w$]*)[ \t]*\??[ \t]*(?:<[^>\n]*>)?[ \t]*(\(|(?::[^=\n;]+)?=[ \t]*(?:async[ \t]+)?(?:\(|[A-Za-z_$][\w$]*[ \t]*=>))/gm;
|
|
275
|
+
for (const c of classes) {
|
|
276
|
+
MEMBER.lastIndex = c.open + 1;
|
|
277
|
+
while ((x = MEMBER.exec(m)) && x.index < c.to) {
|
|
278
|
+
const end = x.index + x[0].length;
|
|
279
|
+
let body = null;
|
|
280
|
+
let paren = null;
|
|
281
|
+
if (x[3] === "(") {
|
|
282
|
+
paren = end - 1;
|
|
283
|
+
body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
|
|
284
|
+
} else if (x[3].endsWith("(")) {
|
|
285
|
+
paren = end - 1;
|
|
286
|
+
body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, true);
|
|
287
|
+
} else {
|
|
288
|
+
body = bodyAfter(m, m.lastIndexOf("=>", end), true);
|
|
289
|
+
}
|
|
290
|
+
if (paren != null) defParens.add(paren);
|
|
291
|
+
// A member with no body is an overload signature or an abstract
|
|
292
|
+
// declaration. Recording it made `NestFactoryStatic.create` three
|
|
293
|
+
// symbols on nestjs/nest and every `ctx` on it an ambiguity.
|
|
294
|
+
if (!body || body.end >= c.to || m[body.open] === ";") {
|
|
295
|
+
if (body && body.end < c.to) MEMBER.lastIndex = Math.max(body.end + 1, end);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const name = x[2].replace(/^#/, "");
|
|
299
|
+
if (!KW.has(name)) {
|
|
300
|
+
const priv = /private|protected/.test(x[1]) || x[2].startsWith("#");
|
|
301
|
+
add({ name, qname: `${c.name}.${name}`, kind: "method", from: x.index, to: body.end, exported: c.exported && !priv, member: true });
|
|
302
|
+
}
|
|
303
|
+
MEMBER.lastIndex = Math.max(body.end + 1, end);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else if (lang === "go") {
|
|
307
|
+
const TYPE = /^type[ \t]+([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]+(?:struct|interface)[ \t]*\{/gm;
|
|
308
|
+
while ((x = TYPE.exec(m))) {
|
|
309
|
+
const open = x.index + x[0].length - 1;
|
|
310
|
+
add({ name: x[1], qname: x[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), exported: /^[A-Z]/.test(x[1]) });
|
|
311
|
+
}
|
|
312
|
+
const FUNC = /^func[ \t]*(?:\([ \t]*\w*[ \t]*\*?[ \t]*([A-Za-z_]\w*)(?:\[[^\]]*\])?[ \t]*\)[ \t]*)?([A-Za-z_]\w*)[ \t]*(?:\[[^\]]*\])?[ \t]*\(/gm;
|
|
313
|
+
while ((x = FUNC.exec(m))) {
|
|
314
|
+
const paren = x.index + x[0].length - 1;
|
|
315
|
+
const close = matchPair(m, paren, "(", ")");
|
|
316
|
+
const nextFunc = m.indexOf("\nfunc", close);
|
|
317
|
+
const brace = m.indexOf("{", close);
|
|
318
|
+
if (brace < 0 || (nextFunc >= 0 && brace > nextFunc)) continue;
|
|
319
|
+
defParens.add(paren);
|
|
320
|
+
const recv = x[1];
|
|
321
|
+
add({
|
|
322
|
+
name: x[2],
|
|
323
|
+
qname: recv ? `${recv}.${x[2]}` : x[2],
|
|
324
|
+
kind: recv ? "method" : "function",
|
|
325
|
+
from: x.index,
|
|
326
|
+
to: matchPair(m, brace, "{", "}"),
|
|
327
|
+
exported: /^[A-Z]/.test(x[2]),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} else if (lang === "java" || lang === "cs") {
|
|
331
|
+
const CLASS = /^[ \t]*((?:(?:public|private|protected|internal|static|abstract|final|sealed|partial|readonly|unsafe)[ \t]+)*)(?:class|interface|enum|record|struct)[ \t]+([A-Za-z_]\w*)[^{;]*\{/gm;
|
|
332
|
+
while ((x = CLASS.exec(m))) {
|
|
333
|
+
const open = x.index + x[0].length - 1;
|
|
334
|
+
const to = matchPair(m, open, "{", "}");
|
|
335
|
+
const c = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: /public/.test(x[1]) };
|
|
336
|
+
classes.push(c);
|
|
337
|
+
add(c);
|
|
338
|
+
}
|
|
339
|
+
const METHOD = /^[ \t]*((?:(?:public|private|protected|internal|static|final|abstract|synchronized|async|virtual|override|sealed|extern|unsafe|new|default|native|strictfp)[ \t]+)*)(?:<[^>\n]+>[ \t]+)?([\w<>\[\],.?]+(?:[ \t]*<[^>\n]*>)?)[ \t]+([A-Za-z_]\w*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
|
|
340
|
+
while ((x = METHOD.exec(m))) {
|
|
341
|
+
const typeWord = x[2].split(/[<.\[]/)[0];
|
|
342
|
+
if (KW.has(typeWord) || KW.has(x[3])) continue;
|
|
343
|
+
const paren = x.index + x[0].length - 1;
|
|
344
|
+
const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
|
|
345
|
+
if (!body || (m[body.open] === ";" && !/abstract|extern|native/.test(x[1]))) continue;
|
|
346
|
+
defParens.add(paren);
|
|
347
|
+
const owner = innermostClass(x.index);
|
|
348
|
+
add({
|
|
349
|
+
name: x[3],
|
|
350
|
+
qname: owner ? `${owner.name}.${x[3]}` : x[3],
|
|
351
|
+
kind: owner ? "method" : "function",
|
|
352
|
+
from: x.index,
|
|
353
|
+
to: body.end,
|
|
354
|
+
exported: /public/.test(x[1]),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const CTOR = /^[ \t]*(?:(?:public|private|protected|internal)[ \t]+)([A-Z]\w*)[ \t]*\(/gm;
|
|
358
|
+
while ((x = CTOR.exec(m))) {
|
|
359
|
+
const owner = innermostClass(x.index);
|
|
360
|
+
if (!owner || owner.name !== x[1]) continue;
|
|
361
|
+
const paren = x.index + x[0].length - 1;
|
|
362
|
+
const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
|
|
363
|
+
if (!body) continue;
|
|
364
|
+
defParens.add(paren);
|
|
365
|
+
add({ name: x[1], qname: `${owner.name}.${x[1]}`, kind: "method", from: x.index, to: body.end, exported: /public/.test(x[0]) });
|
|
366
|
+
}
|
|
367
|
+
} else if (lang === "php") {
|
|
368
|
+
const CLASS = /^[ \t]*(?:(?:abstract|final|readonly)[ \t]+)*(?:class|interface|trait|enum)[ \t]+([A-Za-z_]\w*)[^{;]*\{/gm;
|
|
369
|
+
while ((x = CLASS.exec(m))) {
|
|
370
|
+
const open = x.index + x[0].length - 1;
|
|
371
|
+
const to = matchPair(m, open, "{", "}");
|
|
372
|
+
const c = { name: x[1], qname: x[1], kind: "class", from: x.index, to, open, exported: true };
|
|
373
|
+
classes.push(c);
|
|
374
|
+
add(c);
|
|
375
|
+
}
|
|
376
|
+
const FUNC = /^[ \t]*((?:(?:public|private|protected|static|abstract|final)[ \t]+)*)function[ \t]+&?([A-Za-z_]\w*)[ \t]*\(/gm;
|
|
377
|
+
while ((x = FUNC.exec(m))) {
|
|
378
|
+
const paren = x.index + x[0].length - 1;
|
|
379
|
+
const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
|
|
380
|
+
if (!body) continue;
|
|
381
|
+
defParens.add(paren);
|
|
382
|
+
const owner = innermostClass(x.index);
|
|
383
|
+
add({
|
|
384
|
+
name: x[2],
|
|
385
|
+
qname: owner ? `${owner.name}.${x[2]}` : x[2],
|
|
386
|
+
kind: owner ? "method" : "function",
|
|
387
|
+
from: x.index,
|
|
388
|
+
to: body.end,
|
|
389
|
+
exported: !/private|protected/.test(x[1]),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let exportsSet = null;
|
|
395
|
+
if (lang === "js" || lang === "ts") {
|
|
396
|
+
exportsSet = new Set();
|
|
397
|
+
for (const e of m.matchAll(/^[ \t]*export[ \t]*\{([^}]*)\}/gm))
|
|
398
|
+
for (const p of e[1].split(",")) {
|
|
399
|
+
const mm = /^\s*([\w$]+)/.exec(p);
|
|
400
|
+
if (mm) exportsSet.add(mm[1]);
|
|
401
|
+
}
|
|
402
|
+
for (const e of m.matchAll(/export[ \t]+default[ \t]+([A-Za-z_$][\w$]*)/g)) exportsSet.add(e[1]);
|
|
403
|
+
for (const e of m.matchAll(/(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=/g)) exportsSet.add(e[1]);
|
|
404
|
+
const me = /module\.exports[ \t]*=[ \t]*(\{[^}]*\}|[A-Za-z_$][\w$]*)/.exec(m);
|
|
405
|
+
if (me) {
|
|
406
|
+
if (me[1][0] === "{") {
|
|
407
|
+
for (const p of me[1].slice(1, -1).split(",")) {
|
|
408
|
+
const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?/.exec(p);
|
|
409
|
+
if (mm) exportsSet.add(mm[2] || mm[1]);
|
|
410
|
+
}
|
|
411
|
+
} else exportsSet.add(me[1]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { defs, defParens, exportsSet };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── declarations: Python (indentation) ──────────────────────────────────────
|
|
418
|
+
function pyDefs(src, m, starts) {
|
|
419
|
+
const lines = m.split("\n");
|
|
420
|
+
const defs = [];
|
|
421
|
+
const defParens = new Set();
|
|
422
|
+
const stack = [];
|
|
423
|
+
const width = (s) => s.replace(/\t/g, " ").length;
|
|
424
|
+
for (let i = 0; i < lines.length; i++) {
|
|
425
|
+
const L = lines[i];
|
|
426
|
+
const d = /^([ \t]*)(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)[ \t]*\(/.exec(L);
|
|
427
|
+
const c = d ? null : /^([ \t]*)class[ \t]+([A-Za-z_]\w*)/.exec(L);
|
|
428
|
+
const hit = d || c;
|
|
429
|
+
if (!hit) continue;
|
|
430
|
+
const indent = width(hit[1]);
|
|
431
|
+
while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
432
|
+
let header = i;
|
|
433
|
+
if (d) {
|
|
434
|
+
const paren = starts[i] + hit[0].length - 1;
|
|
435
|
+
defParens.add(paren);
|
|
436
|
+
header = lineOf(starts, matchPair(m, paren, "(", ")")) - 1;
|
|
437
|
+
}
|
|
438
|
+
let end = header;
|
|
439
|
+
for (let k = header + 1; k < lines.length; k++) {
|
|
440
|
+
const t = lines[k];
|
|
441
|
+
if (!t.trim()) continue;
|
|
442
|
+
if (width(/^[ \t]*/.exec(t)[0]) <= indent) break;
|
|
443
|
+
end = k;
|
|
444
|
+
}
|
|
445
|
+
const chain = [];
|
|
446
|
+
for (let s = stack.length - 1; s >= 0 && stack[s].kind === "class"; s--) chain.unshift(stack[s].name);
|
|
447
|
+
const name = hit[2];
|
|
448
|
+
defs.push({
|
|
449
|
+
name,
|
|
450
|
+
qname: [...chain, name].join("."),
|
|
451
|
+
kind: d ? (chain.length ? "method" : "function") : "class",
|
|
452
|
+
s: i + 1,
|
|
453
|
+
e: end + 1,
|
|
454
|
+
exported: !name.startsWith("_"),
|
|
455
|
+
});
|
|
456
|
+
stack.push({ indent, name, kind: d ? "function" : "class" });
|
|
457
|
+
}
|
|
458
|
+
return { defs, defParens };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── calls ───────────────────────────────────────────────────────────────────
|
|
462
|
+
const CALL = /(?:\bnew[ \t]+)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)[ \t]*(?:<[\w$., \t\[\]]*>)?[ \t]*\(/g;
|
|
463
|
+
|
|
464
|
+
function normCall(name) {
|
|
465
|
+
let n = String(name).replace(/[ \t]*(?:\?\.|->|::)[ \t]*/g, ".").replace(/[ \t]+/g, "");
|
|
466
|
+
let self = false;
|
|
467
|
+
const m = /^(?:\$this|this|self|static|cls)\.(.+)$/.exec(n);
|
|
468
|
+
if (m) {
|
|
469
|
+
n = m[1];
|
|
470
|
+
self = true;
|
|
471
|
+
}
|
|
472
|
+
n = n.replace(/^\$/, "").replace(/\.\$/g, ".");
|
|
473
|
+
const last = n.split(".").pop();
|
|
474
|
+
// A keyword after a dot is a METHOD name — `router.delete(`, `promise.catch(` —
|
|
475
|
+
// and the arguments inside it (a middleware passed by name) are still scanned.
|
|
476
|
+
if (!last || (KW.has(last) && !n.includes(".")) || /^\d/.test(last)) return null;
|
|
477
|
+
return { name: n, self };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function scanCalls(m, starts, defParens) {
|
|
481
|
+
const out = [];
|
|
482
|
+
let x;
|
|
483
|
+
CALL.lastIndex = 0;
|
|
484
|
+
while ((x = CALL.exec(m))) {
|
|
485
|
+
const paren = x.index + x[0].length - 1;
|
|
486
|
+
if (defParens.has(paren)) continue;
|
|
487
|
+
// `function (` / `def (` style anonymous forms, and the word before a
|
|
488
|
+
// declaration name, are not calls.
|
|
489
|
+
const before = m.slice(Math.max(0, x.index - 12), x.index);
|
|
490
|
+
if (/\b(function|def|func|fn)[ \t]*\*?[ \t]*$/.test(before)) continue;
|
|
491
|
+
const c = normCall(x[1]);
|
|
492
|
+
if (!c) continue;
|
|
493
|
+
const close = matchPair(m, paren, "(", ")");
|
|
494
|
+
// `async (req, res) => …` is a parameter list, not a call to `async`.
|
|
495
|
+
if (c.name === "async" && /^\s*=>/.test(m.slice(close + 1, close + 8))) continue;
|
|
496
|
+
out.push({ name: c.name, line: lineOf(starts, paren), ...(c.self ? { self: true } : {}), _p: paren });
|
|
497
|
+
refsIn(m, starts, paren, close, out);
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// A function passed BY NAME — `router.post("/", requireAuth, handler)`,
|
|
503
|
+
// `items.map(normalize)` — is a use no call scan sees, so `requireAuth` had
|
|
504
|
+
// fan-in 0 on every route that used it. Each bare or dotted identifier argument
|
|
505
|
+
// becomes a REF. `finalize` keeps a ref only when its head is defined in this
|
|
506
|
+
// file or bound by an import, and resolution accepts only LOCAL or IMPORT for
|
|
507
|
+
// it, so a local variable passed along never becomes an edge.
|
|
508
|
+
const REF_WORDS = new Set(["true", "false", "null", "undefined", "nil", "None", "True", "False", "NaN", "Infinity"]);
|
|
509
|
+
const REF_ARG = /^\s*(?:\.\.\.|&)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)\s*$/;
|
|
510
|
+
|
|
511
|
+
function refsIn(m, starts, open, close, out) {
|
|
512
|
+
let depth = 0;
|
|
513
|
+
let a = open + 1;
|
|
514
|
+
for (let k = open + 1; k <= close && k < m.length; k++) {
|
|
515
|
+
const ch = m[k];
|
|
516
|
+
const end = k === close;
|
|
517
|
+
if (!end && (ch === "(" || ch === "[" || ch === "{")) depth++;
|
|
518
|
+
else if (!end && (ch === ")" || ch === "]" || ch === "}")) depth--;
|
|
519
|
+
if (!end && !(ch === "," && depth === 0)) continue;
|
|
520
|
+
const piece = m.slice(a, k);
|
|
521
|
+
const t = REF_ARG.exec(piece);
|
|
522
|
+
if (t && !REF_WORDS.has(t[1])) {
|
|
523
|
+
const c = normCall(t[1]);
|
|
524
|
+
if (c) out.push({ name: c.name, line: lineOf(starts, a + piece.indexOf(t[1])), ref: true, ...(c.self ? { self: true } : {}) });
|
|
525
|
+
}
|
|
526
|
+
a = k + 1;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── imports ─────────────────────────────────────────────────────────────────
|
|
531
|
+
// A statement is read from the ORIGINAL text (its path is a string), but only
|
|
532
|
+
// where the mask agrees the keyword is code — never inside a comment.
|
|
533
|
+
function codeAt(src, m, idx) {
|
|
534
|
+
let k = idx;
|
|
535
|
+
while (k < src.length && (src[k] === " " || src[k] === "\t")) k++;
|
|
536
|
+
return m[k] === src[k] && m[k] !== " ";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function jsClause(cl) {
|
|
540
|
+
const b = [];
|
|
541
|
+
const brace = /\{([^}]*)\}/.exec(cl);
|
|
542
|
+
if (brace)
|
|
543
|
+
for (const part of brace[1].split(",")) {
|
|
544
|
+
const p = part.trim().replace(/^type\s+/, "");
|
|
545
|
+
const mm = /^([\w$]+)(?:\s+as\s+([\w$]+))?$/.exec(p);
|
|
546
|
+
if (mm) b.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
|
|
547
|
+
}
|
|
548
|
+
for (const r of cl.replace(/\{[^}]*\}/, "").split(",")) {
|
|
549
|
+
const t = r.trim();
|
|
550
|
+
if (!t) continue;
|
|
551
|
+
const ns = /^\*\s+as\s+([\w$]+)$/.exec(t);
|
|
552
|
+
if (ns) b.push({ local: ns[1], orig: "*", kind: "namespace" });
|
|
553
|
+
else if (/^[\w$]+$/.test(t)) b.push({ local: t, orig: "default", kind: "default" });
|
|
554
|
+
}
|
|
555
|
+
return b;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function importsFor(lang, src, m, starts) {
|
|
559
|
+
const out = [];
|
|
560
|
+
const line = (i) => lineOf(starts, i);
|
|
561
|
+
let x;
|
|
562
|
+
if (lang === "js" || lang === "ts") {
|
|
563
|
+
const IMP = /^[ \t]*import[ \t]+(?:type[ \t]+)?([\w$*{}\s,]+?)[ \t]*from[ \t]*['"]([^'"\n]+)['"]/gm;
|
|
564
|
+
while ((x = IMP.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: jsClause(x[1]) });
|
|
565
|
+
const REQ = /(?:const|let|var)[ \t]+(\{[^}]*\}|[A-Za-z_$][\w$]*)[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)(?:\.([A-Za-z_$][\w$]*))?/g;
|
|
566
|
+
while ((x = REQ.exec(src))) {
|
|
567
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
568
|
+
const bindings = [];
|
|
569
|
+
if (x[1][0] === "{") {
|
|
570
|
+
for (const p of x[1].slice(1, -1).split(",")) {
|
|
571
|
+
const mm = /^\s*([\w$]+)\s*(?::\s*([\w$]+))?\s*$/.exec(p);
|
|
572
|
+
if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
|
|
573
|
+
}
|
|
574
|
+
} else if (x[3]) bindings.push({ local: x[1], orig: x[3], kind: "named" });
|
|
575
|
+
else bindings.push({ local: x[1], orig: "default", kind: "namespace" });
|
|
576
|
+
out.push({ from: x[2], line: line(x.index), bindings });
|
|
577
|
+
}
|
|
578
|
+
} else if (lang === "py") {
|
|
579
|
+
const FROM = /^[ \t]*from[ \t]+([.\w]+)[ \t]+import[ \t]+(\([^)]*\)|[^\n#]+)/gm;
|
|
580
|
+
while ((x = FROM.exec(src))) {
|
|
581
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
582
|
+
const bindings = [];
|
|
583
|
+
for (const p of x[2].replace(/[()]/g, "").split(",")) {
|
|
584
|
+
const mm = /^\s*(\w+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
|
|
585
|
+
if (mm) bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
|
|
586
|
+
}
|
|
587
|
+
out.push({ from: x[1], line: line(x.index), bindings });
|
|
588
|
+
}
|
|
589
|
+
const IMP = /^[ \t]*import[ \t]+([^\n#]+)/gm;
|
|
590
|
+
while ((x = IMP.exec(src))) {
|
|
591
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
592
|
+
for (const p of x[1].split(",")) {
|
|
593
|
+
const mm = /^\s*([\w.]+)(?:\s+as\s+(\w+))?\s*$/.exec(p);
|
|
594
|
+
if (!mm) continue;
|
|
595
|
+
const mod = mm[2] ? mm[1] : mm[1].split(".")[0];
|
|
596
|
+
out.push({ from: mod, line: line(x.index), bindings: [{ local: mm[2] || mod, orig: mod, kind: "namespace" }] });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} else if (lang === "go") {
|
|
600
|
+
const addGo = (alias, p, at) => {
|
|
601
|
+
const local = alias || p.split("/").pop();
|
|
602
|
+
out.push({ from: p, line: line(at), bindings: alias === "_" || alias === "." ? [] : [{ local, orig: local, kind: "namespace" }] });
|
|
603
|
+
};
|
|
604
|
+
const ONE = /^import[ \t]+(?:([\w.]+)[ \t]+)?"([^"]+)"/gm;
|
|
605
|
+
while ((x = ONE.exec(src))) if (codeAt(src, m, x.index)) addGo(x[1], x[2], x.index);
|
|
606
|
+
const BLOCK = /^import[ \t]*\(([\s\S]*?)\)/gm;
|
|
607
|
+
while ((x = BLOCK.exec(src))) {
|
|
608
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
609
|
+
let off = x.index + x[0].indexOf("(") + 1;
|
|
610
|
+
for (const l of x[1].split("\n")) {
|
|
611
|
+
const mm = /^\s*(?:([\w.]+)\s+)?"([^"]+)"/.exec(l);
|
|
612
|
+
if (mm) addGo(mm[1], mm[2], off);
|
|
613
|
+
off += l.length + 1;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else if (lang === "java") {
|
|
617
|
+
const IMP = /^[ \t]*import[ \t]+(static[ \t]+)?([\w.]+?)(\.\*)?[ \t]*;/gm;
|
|
618
|
+
while ((x = IMP.exec(src))) {
|
|
619
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
620
|
+
const parts = x[2].split(".");
|
|
621
|
+
if (x[3]) out.push({ from: x[2], line: line(x.index), bindings: [], star: true });
|
|
622
|
+
else if (x[1]) out.push({ from: parts.slice(0, -1).join("."), line: line(x.index), bindings: [{ local: parts[parts.length - 1], orig: parts[parts.length - 1], kind: "named" }] });
|
|
623
|
+
else out.push({ from: x[2], line: line(x.index), bindings: [{ local: parts[parts.length - 1], orig: parts[parts.length - 1], kind: "named" }] });
|
|
624
|
+
}
|
|
625
|
+
} else if (lang === "cs") {
|
|
626
|
+
const USING = /^[ \t]*using[ \t]+(?:static[ \t]+)?(?:(\w+)[ \t]*=[ \t]*)?([\w.]+)[ \t]*;/gm;
|
|
627
|
+
while ((x = USING.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: [] });
|
|
628
|
+
} else if (lang === "php") {
|
|
629
|
+
const USE = /^[ \t]*use[ \t]+(?:function[ \t]+)?([\w\\]+)(?:[ \t]+as[ \t]+(\w+))?[ \t]*;/gm;
|
|
630
|
+
while ((x = USE.exec(src))) {
|
|
631
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
632
|
+
const last = x[1].split("\\").pop();
|
|
633
|
+
out.push({ from: x[1], line: line(x.index), bindings: [{ local: x[2] || last, orig: last, kind: "named" }] });
|
|
634
|
+
}
|
|
635
|
+
const INC = /\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*['"]([^'"\n]+\.php)['"]/g;
|
|
636
|
+
while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ── effects ─────────────────────────────────────────────────────────────────
|
|
642
|
+
const SQL_RE = /^\s*(SELECT\s|INSERT\s+INTO\s|UPDATE\s+[\w."`\[\]]+\s+SET\s|DELETE\s+FROM\s|CREATE\s+(TABLE|INDEX|VIEW)\s|ALTER\s+TABLE\s|DROP\s+TABLE\s|WITH\s+\w+\s+AS\s*\(|MERGE\s+INTO\s)/i;
|
|
643
|
+
const HTTP_RE = /^(fetch|axios|axios\.(get|post|put|patch|delete|head|request)|https?\.(request|get)|requests\.(get|post|put|patch|delete|head|request)|httpx\.(get|post|put|patch|delete|request)|http\.(Get|Post|Head|NewRequest|NewRequestWithContext)|curl_exec|Http\.(get|post|put|patch|delete)|restTemplate\.\w+|webClient\.\w+|httpClient\.(get|post|put|patch|delete|send|GetAsync|PostAsync|PutAsync|DeleteAsync|SendAsync)|urllib\.request\.urlopen|urlopen)$/;
|
|
644
|
+
const DB_RE = /(^[\w$]+(\.[\w$]+)*\.(query|execute|executemany|executescript|raw|\$queryRaw|\$executeRaw|\$queryRawUnsafe|\$executeRawUnsafe)$)|(^prisma\.[\w$]+\.[\w$]+$)|(^knex(\.|$))|(\.objects\.\w+$)|(^(db|conn|connection|pool|cursor|session|sequelize|em|entityManager|dataSource|queryRunner|mongoose|collection)\.[\w$]+$)/;
|
|
645
|
+
const FS_RE = {
|
|
646
|
+
js: /^(fs|fsp|fse|fs\.promises)\.(writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream|mkdir|mkdirSync|rm|rmSync|rmdir|rmdirSync|unlink|unlinkSync|rename|renameSync|copyFile|copyFileSync|outputFile|remove)$/,
|
|
647
|
+
go: /^(os\.(WriteFile|Create|Remove|RemoveAll|Mkdir|MkdirAll|Rename)|ioutil\.WriteFile)$/,
|
|
648
|
+
py: /^(os\.(remove|unlink|rename|makedirs|mkdir|rmdir)|shutil\.(rmtree|move|copy|copyfile)|.+\.(write_text|write_bytes))$/,
|
|
649
|
+
php: /^(file_put_contents|unlink|mkdir|rmdir|rename|fwrite)$/,
|
|
650
|
+
cs: /^(File\.(Write\w*|Delete|Move|Copy|AppendAll\w*|Create)|Directory\.(CreateDirectory|Delete))$/,
|
|
651
|
+
java: /^(Files\.(write|writeString|delete|deleteIfExists|move|copy|createDirectories))$/,
|
|
652
|
+
};
|
|
653
|
+
FS_RE.ts = FS_RE.js;
|
|
654
|
+
const ENV_RE = /process\.env(?:\.([A-Za-z_]\w*)|\[\s*['"`]([^'"`]+)['"`]\s*\])|os\.environ(?:\.get\(\s*|\[\s*)['"]([^'"]+)['"]|os\.getenv\(\s*['"]([^'"]+)['"]|os\.Getenv\(\s*"([^"]+)"|System\.getenv\(\s*"([^"]+)"|Environment\.GetEnvironmentVariable\(\s*"([^"]+)"|\bgetenv\(\s*['"]([^'"]+)['"]|\$_ENV\[\s*['"]([^'"]+)['"]/g;
|
|
655
|
+
|
|
656
|
+
function stringAt(src, strings, pos) {
|
|
657
|
+
let k = pos;
|
|
658
|
+
while (k < src.length && /\s/.test(src[k])) k++;
|
|
659
|
+
if (!/['"`]/.test(src[k] || "")) return null;
|
|
660
|
+
const s = strings.find((t) => t.start === k + 1);
|
|
661
|
+
return s ? src.slice(s.start, s.end) : null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function parenFor(src, starts, call) {
|
|
665
|
+
if (call._p != null) return call._p;
|
|
666
|
+
const ls = starts[call.line - 1] || 0;
|
|
667
|
+
const le = starts[call.line] || src.length;
|
|
668
|
+
const last = call.name.split(".").pop();
|
|
669
|
+
const i = src.indexOf(last, ls);
|
|
670
|
+
if (i < 0 || i >= le) return null;
|
|
671
|
+
const p = src.indexOf("(", i + last.length);
|
|
672
|
+
return p >= 0 && p < le ? p : null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function effectsOf(lang, src, m, starts, strings, calls) {
|
|
676
|
+
const out = [];
|
|
677
|
+
for (const s of strings) {
|
|
678
|
+
const text = src.slice(s.start, s.end);
|
|
679
|
+
if (SQL_RE.test(text)) out.push({ type: "sql", text: clip(text), line: lineOf(starts, s.start) });
|
|
680
|
+
}
|
|
681
|
+
let x;
|
|
682
|
+
ENV_RE.lastIndex = 0;
|
|
683
|
+
while ((x = ENV_RE.exec(src))) {
|
|
684
|
+
if (!codeAt(src, m, x.index)) continue;
|
|
685
|
+
const name = x.slice(1).find(Boolean);
|
|
686
|
+
if (name) out.push({ type: "env", text: name, line: lineOf(starts, x.index) });
|
|
687
|
+
}
|
|
688
|
+
const fsRe = FS_RE[lang];
|
|
689
|
+
for (const c of calls) {
|
|
690
|
+
if (HTTP_RE.test(c.name)) {
|
|
691
|
+
const p = parenFor(src, starts, c);
|
|
692
|
+
const url = p != null ? stringAt(src, strings, p + 1) : null;
|
|
693
|
+
out.push({ type: "http", text: url ? `${c.name} ${clip(url, 60)}` : c.name, line: c.line });
|
|
694
|
+
} else if (DB_RE.test(c.name)) {
|
|
695
|
+
out.push({ type: "db", text: c.name, line: c.line });
|
|
696
|
+
} else if (fsRe && fsRe.test(c.name)) {
|
|
697
|
+
out.push({ type: "fs", text: c.name, line: c.line });
|
|
698
|
+
} else if (lang === "py" && c.name === "open") {
|
|
699
|
+
const p = parenFor(src, starts, c);
|
|
700
|
+
if (p != null && /^[^)]*?,\s*(?:mode\s*=\s*)?['"][wax]/.test(src.slice(p + 1, p + 120))) out.push({ type: "fs", text: "open (write)", line: c.line });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── assembly ────────────────────────────────────────────────────────────────
|
|
707
|
+
function finalize(rel, lang, src, m, starts, strings, defs, calls, imports, exportsSet, extractor) {
|
|
708
|
+
const nLines = starts.length;
|
|
709
|
+
const owner = new Int32Array(nLines + 2).fill(-1);
|
|
710
|
+
const order = defs.map((_, i) => i).sort((a, b) => defs[b].e - defs[b].s - (defs[a].e - defs[a].s));
|
|
711
|
+
for (const i of order) for (let L = defs[i].s; L <= defs[i].e && L <= nLines; L++) owner[L] = i;
|
|
712
|
+
|
|
713
|
+
const syms = defs.map((d) => ({
|
|
714
|
+
id: "",
|
|
715
|
+
name: d.name,
|
|
716
|
+
qname: d.qname || d.name,
|
|
717
|
+
kind: d.kind,
|
|
718
|
+
lines: [d.s, d.e],
|
|
719
|
+
exported: !!(d.exported || (exportsSet && exportsSet.has(d.name))),
|
|
720
|
+
body_hash: "",
|
|
721
|
+
calls: [],
|
|
722
|
+
effects: [],
|
|
723
|
+
}));
|
|
724
|
+
const mod = { id: "", name: "<module>", qname: "<module>", kind: "module", lines: [1, nLines], exported: false, body_hash: "", calls: [], effects: [] };
|
|
725
|
+
const at = (line) => (owner[line] >= 0 ? syms[owner[line]] : mod);
|
|
726
|
+
|
|
727
|
+
const known = new Set(defs.map((d) => d.name));
|
|
728
|
+
for (const imp of imports || []) for (const b of imp.bindings || []) known.add(b.local);
|
|
729
|
+
// A cap is a coverage gap, and the gap starts at the FIRST thing that was
|
|
730
|
+
// dropped — not at the top of the symbol. A file whose `<module>` holds 400
|
|
731
|
+
// calls is fully seen down to call 200; saying the whole file is partial
|
|
732
|
+
// would hide where the record actually stops.
|
|
733
|
+
const cutAt = new Map();
|
|
734
|
+
const cut = (t, line) => {
|
|
735
|
+
const was = cutAt.get(t);
|
|
736
|
+
if (was === undefined || line < was) cutAt.set(t, line);
|
|
737
|
+
};
|
|
738
|
+
for (const c of calls) {
|
|
739
|
+
if (c.ref && !known.has(c.self ? c.name.split(".").pop() : c.name.split(".")[0])) continue;
|
|
740
|
+
const t = at(c.line);
|
|
741
|
+
if (t.calls.length < MAX_CALLS_PER_SYMBOL)
|
|
742
|
+
t.calls.push({ name: c.name, line: c.line, ...(c.self ? { self: true } : {}), ...(c.ref ? { ref: true } : {}) });
|
|
743
|
+
else cut(t, c.line);
|
|
744
|
+
}
|
|
745
|
+
// A function passed as an argument runs no HTTP, SQL or fs effect by being passed.
|
|
746
|
+
for (const ef of effectsOf(lang, src, m, starts, strings, calls.filter((c) => !c.ref))) {
|
|
747
|
+
const t = at(ef.line);
|
|
748
|
+
if (t.effects.length >= MAX_EFFECTS_PER_SYMBOL) {
|
|
749
|
+
cut(t, ef.line);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (!t.effects.some((e) => e.type === ef.type && e.text === ef.text)) t.effects.push(ef);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// The partial RANGES, merged, in line order. A cap gives the tail from the
|
|
756
|
+
// first dropped line; an unbalanced block gives everything from it to the end
|
|
757
|
+
// of the file.
|
|
758
|
+
const ranges = [];
|
|
759
|
+
for (const [t, line] of cutAt) ranges.push([line, t.lines[1]]);
|
|
760
|
+
if (UNBALANCED >= 0) ranges.push([lineOf(starts, UNBALANCED), nLines]);
|
|
761
|
+
ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
762
|
+
const partial = [];
|
|
763
|
+
for (const r of ranges) {
|
|
764
|
+
const last = partial[partial.length - 1];
|
|
765
|
+
if (last && r[0] <= last[1] + 1) last[1] = Math.max(last[1], r[1]);
|
|
766
|
+
else partial.push([r[0], r[1]]);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const seen = new Map();
|
|
770
|
+
for (const s of syms) {
|
|
771
|
+
const a = starts[s.lines[0] - 1] || 0;
|
|
772
|
+
const b = s.lines[1] < nLines ? starts[s.lines[1]] : src.length;
|
|
773
|
+
s.body_hash = hash(src.slice(a, b));
|
|
774
|
+
let id = `${rel}#${s.qname}`;
|
|
775
|
+
const k = seen.get(id) || 0;
|
|
776
|
+
seen.set(id, k + 1);
|
|
777
|
+
if (k) id += `~${k + 1}`;
|
|
778
|
+
s.id = id;
|
|
779
|
+
}
|
|
780
|
+
if (mod.calls.length || mod.effects.length) {
|
|
781
|
+
mod.id = `${rel}#<module>`;
|
|
782
|
+
syms.push(mod);
|
|
783
|
+
}
|
|
784
|
+
return { extractor, imports, symbols: syms, coverage: partial.length ? "partial" : "full", ...(partial.length ? { partial } : {}) };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function extractOne(item, borrowed) {
|
|
788
|
+
const { rel, lang, src } = item;
|
|
789
|
+
resetCoverage();
|
|
790
|
+
const { masked: m, strings } = mask(src, lang);
|
|
791
|
+
const starts = lineIndex(src);
|
|
792
|
+
if (borrowed) {
|
|
793
|
+
const calls = [];
|
|
794
|
+
for (const c of borrowed.calls) {
|
|
795
|
+
const n = normCall(c.name);
|
|
796
|
+
if (n) calls.push({ name: n.name, line: c.line, ...(n.self ? { self: true } : {}), ...(c.ref ? { ref: true } : {}) });
|
|
797
|
+
}
|
|
798
|
+
return finalize(rel, lang, src, m, starts, strings, borrowed.defs, calls, borrowed.imports, null, borrowed.extractor);
|
|
799
|
+
}
|
|
800
|
+
if (lang === "py") {
|
|
801
|
+
const { defs, defParens } = pyDefs(src, m, starts);
|
|
802
|
+
return finalize(rel, lang, src, m, starts, strings, defs, scanCalls(m, starts, defParens), importsFor(lang, src, m, starts), null, HEURISTIC);
|
|
803
|
+
}
|
|
804
|
+
const { defs, defParens, exportsSet } = braceDefs(src, m, starts, lang, strings);
|
|
805
|
+
return finalize(rel, lang, src, m, starts, strings, defs, scanCalls(m, starts, defParens), importsFor(lang, src, m, starts), exportsSet, HEURISTIC);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ── the borrowed Python parser ──────────────────────────────────────────────
|
|
809
|
+
const PY_AST = String.raw`
|
|
810
|
+
import ast, sys, json
|
|
811
|
+
def dotted(n):
|
|
812
|
+
parts = []
|
|
813
|
+
while isinstance(n, ast.Attribute):
|
|
814
|
+
parts.append(n.attr)
|
|
815
|
+
n = n.value
|
|
816
|
+
if isinstance(n, ast.Name):
|
|
817
|
+
parts.append(n.id)
|
|
818
|
+
return ".".join(reversed(parts))
|
|
819
|
+
return None
|
|
820
|
+
class V(ast.NodeVisitor):
|
|
821
|
+
def __init__(s):
|
|
822
|
+
s.defs = []; s.calls = []; s.imports = []; s.stack = []
|
|
823
|
+
def qual(s, name):
|
|
824
|
+
chain = []
|
|
825
|
+
for kind, n in reversed(s.stack):
|
|
826
|
+
if kind != "class":
|
|
827
|
+
break
|
|
828
|
+
chain.append(n)
|
|
829
|
+
chain.reverse()
|
|
830
|
+
return ".".join(chain + [name]), bool(chain)
|
|
831
|
+
def visit_ClassDef(s, node):
|
|
832
|
+
q, _ = s.qual(node.name)
|
|
833
|
+
s.defs.append({"name": node.name, "qname": q, "kind": "class", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")})
|
|
834
|
+
s.stack.append(("class", node.name)); s.generic_visit(node); s.stack.pop()
|
|
835
|
+
def visit_FunctionDef(s, node):
|
|
836
|
+
q, inclass = s.qual(node.name)
|
|
837
|
+
s.defs.append({"name": node.name, "qname": q, "kind": "method" if inclass else "function", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")})
|
|
838
|
+
s.stack.append(("function", node.name)); s.generic_visit(node); s.stack.pop()
|
|
839
|
+
visit_AsyncFunctionDef = visit_FunctionDef
|
|
840
|
+
def visit_Call(s, node):
|
|
841
|
+
name = dotted(node.func)
|
|
842
|
+
if name:
|
|
843
|
+
s.calls.append({"name": name, "line": node.lineno})
|
|
844
|
+
for a in list(node.args) + [k.value for k in node.keywords]:
|
|
845
|
+
r = dotted(a)
|
|
846
|
+
if r:
|
|
847
|
+
s.calls.append({"name": r, "line": getattr(a, "lineno", node.lineno), "ref": True})
|
|
848
|
+
s.generic_visit(node)
|
|
849
|
+
def visit_Import(s, node):
|
|
850
|
+
for a in node.names:
|
|
851
|
+
mod = a.name if a.asname else a.name.split(".")[0]
|
|
852
|
+
s.imports.append({"from": mod, "line": node.lineno, "bindings": [{"local": a.asname or mod, "orig": mod, "kind": "namespace"}]})
|
|
853
|
+
def visit_ImportFrom(s, node):
|
|
854
|
+
mod = "." * (node.level or 0) + (node.module or "")
|
|
855
|
+
b = [{"local": a.asname or a.name, "orig": a.name, "kind": "named"} for a in node.names if a.name != "*"]
|
|
856
|
+
s.imports.append({"from": mod, "line": node.lineno, "bindings": b})
|
|
857
|
+
items = json.loads(sys.stdin.buffer.read().decode("utf-8"))
|
|
858
|
+
out = {}
|
|
859
|
+
for it in items:
|
|
860
|
+
try:
|
|
861
|
+
with open(it["abs"], "r", encoding="utf-8", errors="replace") as f:
|
|
862
|
+
tree = ast.parse(f.read())
|
|
863
|
+
v = V(); v.visit(tree)
|
|
864
|
+
out[it["rel"]] = {"defs": v.defs, "calls": v.calls, "imports": v.imports}
|
|
865
|
+
except Exception:
|
|
866
|
+
out[it["rel"]] = None
|
|
867
|
+
sys.stdout.buffer.write(json.dumps(out).encode("utf-8"))
|
|
868
|
+
`;
|
|
869
|
+
|
|
870
|
+
let pythonProbe;
|
|
871
|
+
function findPython() {
|
|
872
|
+
if (pythonProbe !== undefined) return pythonProbe;
|
|
873
|
+
pythonProbe = null;
|
|
874
|
+
const cands = [];
|
|
875
|
+
if (process.env.ORC_GRAPH_PYTHON) cands.push([process.env.ORC_GRAPH_PYTHON]);
|
|
876
|
+
cands.push(["python3"], ["python"], ["py", "-3"]);
|
|
877
|
+
for (const [cmd, ...pre] of cands) {
|
|
878
|
+
const r = spawnSync(cmd, [...pre, "-c", "import sys;print('%d.%d' % sys.version_info[:2]);print(sys.version_info >= (3, 8))"], {
|
|
879
|
+
encoding: "utf8",
|
|
880
|
+
timeout: 10000,
|
|
881
|
+
windowsHide: true,
|
|
882
|
+
});
|
|
883
|
+
if (r.status === 0 && /True/.test(r.stdout || "")) {
|
|
884
|
+
pythonProbe = { cmd, pre, ver: (r.stdout || "").split(/\r?\n/)[0].trim() };
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return pythonProbe;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function runPythonAst(items) {
|
|
892
|
+
const py = findPython();
|
|
893
|
+
if (!py) return null;
|
|
894
|
+
const r = spawnSync(py.cmd, [...py.pre, "-c", PY_AST], {
|
|
895
|
+
input: JSON.stringify(items.map((i) => ({ rel: i.rel, abs: i.abs }))),
|
|
896
|
+
encoding: "utf8",
|
|
897
|
+
maxBuffer: 512 * 1024 * 1024,
|
|
898
|
+
timeout: 300000,
|
|
899
|
+
windowsHide: true,
|
|
900
|
+
});
|
|
901
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
902
|
+
try {
|
|
903
|
+
return { map: JSON.parse(r.stdout), extractor: `python-ast@${py.ver}` };
|
|
904
|
+
} catch (_) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// items: [{ rel, abs, lang, src }] → Map<rel, { extractor, imports, symbols }>
|
|
910
|
+
function extractBatch(items) {
|
|
911
|
+
const out = new Map();
|
|
912
|
+
const pys = items.filter((i) => i.lang === "py");
|
|
913
|
+
const ast = pys.length && !process.env.ORC_GRAPH_NO_BORROW ? runPythonAst(pys) : null;
|
|
914
|
+
for (const it of items) {
|
|
915
|
+
const b = ast && ast.map[it.rel];
|
|
916
|
+
try {
|
|
917
|
+
out.set(it.rel, extractOne(it, b ? { ...b, extractor: ast.extractor } : null));
|
|
918
|
+
} catch (_) {
|
|
919
|
+
// A file the extractor cannot read is an empty record, never a failed
|
|
920
|
+
// update: one pathological file must not take the whole map down.
|
|
921
|
+
out.set(it.rel, { extractor: HEURISTIC, error: "extract-failed", imports: [], symbols: [] });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
module.exports = { HEURISTIC, MAX_CALLS_PER_SYMBOL, MAX_EFFECTS_PER_SYMBOL, extractBatch, extractOne, mask, normCall, _findPython: findPython };
|