@azure-id/orc 1.8.1 → 1.8.2

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/README-id.md +90 -71
  3. package/README.md +77 -31
  4. package/bin/cli.js +45460 -44867
  5. package/bin/graph-extract.js +2409 -120
  6. package/bin/graph-gain.js +404 -0
  7. package/bin/graph-map.js +232 -0
  8. package/bin/graph-notes.js +49 -8
  9. package/bin/graph-query.js +1770 -808
  10. package/bin/graph-resolve.js +93 -16
  11. package/bin/graph-shard.js +325 -0
  12. package/bin/graph.js +658 -605
  13. package/bin/verify-contracts.js +140 -0
  14. package/bin/verify-package.js +14 -0
  15. package/bin/webui/api.js +6 -0
  16. package/bin/webui/fixtures/index.js +6 -1
  17. package/bin/webui/fixtures/knowledge.js +41 -1
  18. package/bin/webui/fixtures/stats.js +107 -104
  19. package/bin/webui/i18n/en/knowledge.json +16 -1
  20. package/bin/webui/i18n/id/knowledge.json +16 -1
  21. package/bin/webui/js/panels/knowledge.js +68 -3
  22. package/package.json +1 -1
  23. package/templates/agents/orc-executor-haiku-4-5.md +8 -13
  24. package/templates/agents/orc-executor-opus-4-7-high.md +8 -13
  25. package/templates/agents/orc-executor-opus-4-7-med.md +8 -13
  26. package/templates/agents/orc-executor-opus-4-8-high.md +8 -13
  27. package/templates/agents/orc-executor-opus-5-high.md +8 -13
  28. package/templates/agents/orc-executor-opus-5-low.md +8 -13
  29. package/templates/agents/orc-executor-opus-5-med.md +8 -13
  30. package/templates/agents/orc-executor-sonnet-4-6-high.md +8 -13
  31. package/templates/agents/orc-executor-sonnet-4-6-med.md +8 -13
  32. package/templates/agents/orc-executor-sonnet-5-high.md +8 -13
  33. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +15 -12
  34. package/templates/hooks/README.md +13 -3
  35. package/templates/hooks/orc-graph-hook.js +148 -13
  36. package/templates/skills/_shared/code-graph.md +148 -20
  37. package/templates/skills/_shared/phases/execution.md +13 -11
  38. package/templates/skills/_shared/phases/planning.md +8 -1
  39. package/templates/skills/_shared/phases/ship.md +5 -1
  40. package/templates/skills/_shared/phases/trace.md +2 -0
  41. package/templates/skills/_shared/phases/wiki-consult.md +1 -1
  42. package/templates/skills/_shared/read-ladder.md +10 -2
  43. package/templates/skills/orc/SKILL.md +1 -1
  44. package/templates/skills/orc-analyze/SKILL.md +13 -13
  45. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  46. package/templates/skills/orc-wiki/references/staleness.md +1 -1
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- // ── orc graph — EXTRACTION (v1.8.0 W2) ─────────────────────────────────────
2
+ // ── orc graph — EXTRACTION (v1.8.0 W2 · v1.8.2 W1) ──────────────────────────
3
3
  //
4
4
  // One source file in, one record out: imports, symbols (with line ranges and a
5
5
  // body hash), the calls each symbol makes, and the side effects it has (SQL,
@@ -25,15 +25,41 @@
25
25
  // is computed on read.
26
26
  // - dynamic dispatch, reflection, macros and eval are invisible. The graph is
27
27
  // a LOCATOR; the code is still the truth.
28
+ //
29
+ // v1.8.2 W1 — ROUTES ARE EDGES (R3-C). A caller that reaches a symbol through a
30
+ // URL had no edge, and the card was silent about eleven route-level tests. Four
31
+ // additions, all NAMES again, resolved on read by `graph-query.js`:
32
+ // - `urls[]` on a symbol: every string literal that is the URL of an
33
+ // HTTP-shaped call (`request(app).get("/p")`, `client.post("/p")`,
34
+ // `fetch("/p", {method})`, `httptest.NewRequest("GET", "/p")`), with its
35
+ // method. A `BASE + "/p"` is recorded as a SUFFIX (prefix unknown).
36
+ // - `mounts[]` on a module symbol: `app.use("/orders", ordersRouter)`,
37
+ // `include_router(r, prefix=)`, `register_blueprint(bp, url_prefix=)`,
38
+ // Django `path("api/", include("x"))` — a prefix and the NAME it mounts.
39
+ // - decorator routes: `@Get(":id")`, `@router.get("/p")`, `@GetMapping`,
40
+ // `[HttpGet]`, `#[Route]` — the decorated function KEEPS its symbol and gains
41
+ // `route`; an alias ROUTE symbol named `<METHOD> <path>` points at it
42
+ // (`handler`) and CALLS it, so every walk reaches the handler with no
43
+ // special case. A same-file or same-class prefix (`@Controller("orders")`,
44
+ // `APIRouter(prefix=)`, `Blueprint(url_prefix=)`) is folded into the name
45
+ // here; a prefix from ANOTHER file is a mount, applied on read.
46
+ // - registration routes with a handler passed BY NAME: `mux.HandleFunc("/p",
47
+ // h)`, `r.GET("/p", h)`, `Route::get('/p', [C::class, 'm'])`, Django
48
+ // `path("p/", views.x)` and Express `router.get("/p", handler)`.
28
49
 
29
50
  const crypto = require("crypto");
51
+ const fs = require("fs");
52
+ const os = require("os");
53
+ const path = require("path");
30
54
  const { spawnSync } = require("child_process");
31
55
 
32
56
  // @3 (W9): route handlers are symbols, and a function passed BY NAME is a `ref`.
33
57
  // @4 (EW1): every record says how much of the file the extractor actually saw.
34
- const HEURISTIC = "heuristic@4";
58
+ // @5 (v1.8.2 W1): urls, mounts, decorator routes, `handler` on a route alias.
59
+ const HEURISTIC = "heuristic@5";
35
60
  const MAX_CALLS_PER_SYMBOL = 200;
36
61
  const MAX_EFFECTS_PER_SYMBOL = 12;
62
+ const MAX_URLS_PER_SYMBOL = 200;
37
63
 
38
64
  const KW = new Set(
39
65
  (
@@ -45,6 +71,35 @@ const KW = new Set(
45
71
  ).split(" ")
46
72
  );
47
73
 
74
+ // A declaration NAME is checked against its own language's keywords only. The
75
+ // shared `KW` set above is for CALL heads, where `list(` in Python is a
76
+ // builtin — but `list()` is an ordinary method on a TypeScript controller,
77
+ // and W1's Nest fixture lost it to the PHP `list` construct.
78
+ const LANG_KW = {
79
+ js: new Set("if for while switch catch function return typeof await yield case new throw else do try instanceof void this super with class export import default extends".split(" ")),
80
+ go: new Set("func if for switch case return range go defer select type var const map chan struct interface".split(" ")),
81
+ java: new Set("if for while switch catch return new throw try do synchronized this super".split(" ")),
82
+ cs: new Set("if for while switch catch return new throw try do using lock fixed foreach this base".split(" ")),
83
+ php: new Set("if for while switch catch return new throw foreach isset empty unset array list print echo require include function".split(" ")),
84
+ // W5 (G6). Each list is that language's control words only — a name a real
85
+ // declaration can never carry. `get` is a Ktor route and a Ruby method, so
86
+ // it is never in one of these sets.
87
+ rb: new Set("if elsif unless while until for case when begin rescue ensure end def class module do then return yield super self nil true false and or not".split(" ")),
88
+ rs: new Set("if else match loop while for return break continue let mut fn impl struct enum trait mod use pub crate self super where as move ref unsafe".split(" ")),
89
+ kt: new Set("if else when while for return break continue val var fun class object interface is in as by try catch finally throw this super companion".split(" ")),
90
+ c: new Set("if else switch case for while do return break continue goto sizeof typedef struct union enum static extern const inline register volatile new delete throw try catch template typename namespace using operator".split(" ")),
91
+ };
92
+ LANG_KW.ts = LANG_KW.js;
93
+ LANG_KW.vue = LANG_KW.js;
94
+ LANG_KW.svelte = LANG_KW.js;
95
+ const isKw = (lang, name) => (LANG_KW[lang] || KW).has(name);
96
+ // A class MEMBER can be called `delete`, `get`, `of` or `list`; only a control
97
+ // word that a failed body skip could expose is refused there.
98
+ const MEMBER_KW = new Set("if for while switch catch return function typeof throw else do try".split(" "));
99
+
100
+ const HTTP_VERBS = new Set(["get", "post", "put", "patch", "delete", "del", "head", "options"]);
101
+ const VERB_OF = (v) => (v === "del" ? "DELETE" : String(v).toUpperCase());
102
+
48
103
  // ── text utilities ──────────────────────────────────────────────────────────
49
104
  function lineIndex(src) {
50
105
  const starts = [0];
@@ -70,14 +125,113 @@ const clip = (s, n = 80) => {
70
125
 
71
126
  const hash = (s) => crypto.createHash("sha1").update(String(s).replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
72
127
 
128
+ // ── N1 (v1.8.2 W4): doc notes at 0 model tokens ─────────────────────────────
129
+ // Layer 2 pays a subagent for one sentence about a function. For a documented
130
+ // function that sentence is ALREADY THERE — a docstring, a JSDoc block, a
131
+ // `///` run, a `#` run — and the extractor is reading the bytes anyway.
132
+ //
133
+ // It is a NOTE, not a fact. A comment can lie and a comment can rot, which is
134
+ // why the precedence line puts `doc` beside graph notes, below a stale wiki,
135
+ // and why a MODEL note outranks the doc note for the same symbol. The body
136
+ // hash rule is the same: a doc is shown only while the body hashes the same,
137
+ // and since it is re-extracted with the body it can never be stale on its own.
138
+ const DOC_MAX = 240;
139
+ // The first line that is a TAG ends the sentence: `@param`, `:returns:`,
140
+ // `Args:`. Nobody wants half a parameter table as the summary of a function.
141
+ const DOC_TAG = /^\s*(?:[@\\][A-Za-z]|:(?:param|type|returns?|raises?|rtype|arg)\b|(?:Args|Arguments|Returns?|Raises?|Yields?|Params?|Parameters|Example|Examples|Note|Notes|Attributes|See Also|Usage|Todo|TODO)\s*:|[-=*_]{3,}\s*$)/;
142
+
143
+ function docText(lines) {
144
+ const buf = [];
145
+ for (const raw of lines) {
146
+ if (DOC_TAG.test(raw)) break;
147
+ buf.push(raw);
148
+ }
149
+ let t = buf.join(" ").replace(/\s+/g, " ").trim();
150
+ if (!t) return "";
151
+ // The FIRST sentence. `Math.max` guards an abbreviation at the very start.
152
+ const m = /^([\s\S]*?[.!?])(?:\s|$)/.exec(t);
153
+ if (m && m[1].length >= 12) t = m[1];
154
+ t = t.replace(/^[*/#\s]+/, "").trim();
155
+ return t.length > DOC_MAX ? t.slice(0, DOC_MAX - 1) + "…" : t;
156
+ }
157
+
158
+ // Python: the docstring is the first thing INSIDE the body.
159
+ function pyDoc(lines, d) {
160
+ let i = d.s - 1; // 0-based index of the `def` line
161
+ const lim = Math.min(lines.length, d.s + 8);
162
+ while (i < lim && !/:\s*(?:#.*)?$/.test(lines[i] || "")) i++;
163
+ i++;
164
+ while (i < lim && !(lines[i] || "").trim()) i++;
165
+ const first = (lines[i] || "").trim();
166
+ const q = /^[rRbBuUfF]{0,2}("""|''')/.exec(first);
167
+ if (!q) return "";
168
+ const open = first.indexOf(q[1]) + 3;
169
+ const rest = first.slice(open);
170
+ const close = rest.indexOf(q[1]);
171
+ if (close >= 0) return docText([rest.slice(0, close)]);
172
+ const buf = [rest];
173
+ for (let j = i + 1; j < Math.min(lines.length, i + 16); j++) {
174
+ const ln = lines[j] || "";
175
+ const e = ln.indexOf(q[1]);
176
+ if (e >= 0) {
177
+ buf.push(ln.slice(0, e));
178
+ break;
179
+ }
180
+ buf.push(ln);
181
+ }
182
+ return docText(buf);
183
+ }
184
+
185
+ // Everything else: the comment block immediately ABOVE, past any decorators.
186
+ function aboveDoc(lines, d) {
187
+ let i = d.s - 2; // 0-based index of the line above the definition
188
+ const skip = (t) => !t || /^@/.test(t) || /^\[[A-Za-z]/.test(t) || /^#\[/.test(t);
189
+ while (i >= 0 && skip((lines[i] || "").trim())) i--;
190
+ if (i < 0) return "";
191
+ const t = (lines[i] || "").trim();
192
+ if (/\*\/$/.test(t)) {
193
+ const buf = [];
194
+ let j = i;
195
+ for (; j >= 0 && j > i - 40; j--) {
196
+ const s = (lines[j] || "").trim();
197
+ buf.unshift(s.replace(/\*\/\s*$/, "").replace(/^\/\*+!?/, "").replace(/^\*+\/?\s?/, ""));
198
+ if (/^\/\*/.test(s)) break;
199
+ }
200
+ return j < 0 ? "" : docText(buf);
201
+ }
202
+ const head = /^\/\//.test(t) ? "//" : /^#/.test(t) ? "#" : /^--/.test(t) ? "--" : null;
203
+ if (!head) return "";
204
+ const strip = head === "//" ? /^\/\/+[!/]?\s?/ : head === "#" ? /^#+!?\s?/ : /^-{2,}!?\s?/;
205
+ const buf = [];
206
+ for (let j = i; j >= 0 && j > i - 20 && (lines[j] || "").trim().startsWith(head); j--) buf.unshift((lines[j] || "").trim().replace(strip, ""));
207
+ return docText(buf);
208
+ }
209
+
210
+ const DOC_KINDS = new Set(["function", "method", "route"]);
211
+
212
+ function docFor(lines, lang, d) {
213
+ if (!DOC_KINDS.has(d.kind)) return "";
214
+ try {
215
+ // Python first looks INSIDE (the docstring), then above (a `#` block) —
216
+ // measured on django, the `#` block above is the only other place a Python
217
+ // author writes the sentence.
218
+ if (lang === "py") return pyDoc(lines, d) || aboveDoc(lines, d) || "";
219
+ return aboveDoc(lines, d) || "";
220
+ } catch (_) {
221
+ return ""; // a doc is a nicety; it never fails an extraction
222
+ }
223
+ }
224
+
73
225
  // Replace comment and string CONTENT with spaces, keeping every newline and
74
226
  // every offset. Declarations and calls are found in the mask; string text (for
75
227
  // SQL and URLs) is read back from the original at the recorded spans.
76
228
  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";
229
+ const slash = lang !== "py" && lang !== "rb";
230
+ const hashC = lang === "py" || lang === "php" || lang === "rb";
231
+ const block = lang !== "py" && lang !== "rb";
232
+ // A Ruby backtick runs a shell command, so its content is a string. A Kotlin
233
+ // backtick quotes an IDENTIFIER (`fun \`adds an order\`()`), so it is code.
234
+ const backtick = lang === "js" || lang === "ts" || lang === "go" || lang === "rb";
81
235
  const m = src.split("");
82
236
  const n = src.length;
83
237
  const strings = [];
@@ -102,6 +256,44 @@ function mask(src, lang) {
102
256
  i = end;
103
257
  continue;
104
258
  }
259
+ // W5 (G6). Ruby `=begin` … `=end` is a block comment, at column 0 only.
260
+ if (lang === "rb" && src.startsWith("=begin", i) && (i === 0 || src[i - 1] === "\n")) {
261
+ const e = src.indexOf("\n=end", i);
262
+ const end = e < 0 ? n : src.indexOf("\n", e + 1) < 0 ? n : src.indexOf("\n", e + 1);
263
+ blank(i, end);
264
+ i = end;
265
+ continue;
266
+ }
267
+ // W5 (G6). A Rust raw string `r"…"` / `r#"…"#`, and a Rust LIFETIME or
268
+ // char literal. `&'a str` opens a quote that never closes, and without
269
+ // this rule the mask blanks the rest of the line — the same class of loss
270
+ // G5 fixed for regex literals.
271
+ if (lang === "rs" && c === "r" && (c2 === '"' || c2 === "#")) {
272
+ let h = 0;
273
+ while (src[i + 1 + h] === "#") h++;
274
+ if (src[i + 1 + h] === '"') {
275
+ const close = '"' + "#".repeat(h);
276
+ const e = src.indexOf(close, i + 2 + h);
277
+ const end = e < 0 ? n : e;
278
+ strings.push({ start: i + 2 + h, end });
279
+ blank(i + 2 + h, end);
280
+ i = e < 0 ? n : e + close.length;
281
+ continue;
282
+ }
283
+ }
284
+ if (lang === "rs" && c === "'" && /[A-Za-z_]/.test(c2 || "") && src[i + 2] !== "'") {
285
+ i++;
286
+ continue;
287
+ }
288
+ // W5 (G6). A Kotlin raw string is `"""…"""`.
289
+ if (lang === "kt" && src.startsWith('"""', i)) {
290
+ const e = src.indexOf('"""', i + 3);
291
+ const end = e < 0 ? n : e;
292
+ strings.push({ start: i + 3, end });
293
+ blank(i + 3, end);
294
+ i = e < 0 ? n : e + 3;
295
+ continue;
296
+ }
105
297
  if (lang === "py" && (src.startsWith('"""', i) || src.startsWith("'''", i))) {
106
298
  const q = src.substr(i, 3);
107
299
  const e = src.indexOf(q, i + 3);
@@ -111,10 +303,53 @@ function mask(src, lang) {
111
303
  i = e < 0 ? n : e + 3;
112
304
  continue;
113
305
  }
306
+ // W2 (G5): a regex literal is masked like a string, so the `/` inside
307
+ // `/\/\*/` cannot open a bogus comment and `str.match(/x/)` is one call.
308
+ // Only where a regex can start: after an operator, a bracket, a comma, a
309
+ // line start, or a keyword — never after a name, a number or `)` (division).
310
+ if ((lang === "js" || lang === "ts") && c === "/" && c2 !== "/" && c2 !== "*") {
311
+ let k = i - 1;
312
+ while (k >= 0 && (m[k] === " " || m[k] === "\t")) k--;
313
+ const prev = k >= 0 ? m[k] : "\n";
314
+ let ok = k < 0 || "(,=:[!&|?{};\n+-*%<>~^".includes(prev);
315
+ if (!ok && /[A-Za-z_$]/.test(prev)) {
316
+ let w = k;
317
+ while (w >= 0 && /[A-Za-z_$]/.test(m[w])) w--;
318
+ ok = /^(return|typeof|instanceof|in|of|delete|void|throw|case|do|else|yield|await)$/.test(m.slice(w + 1, k + 1).join(""));
319
+ }
320
+ if (ok) {
321
+ let j = i + 1;
322
+ let cls = false;
323
+ while (j < n && src[j] !== "\n") {
324
+ const d = src[j];
325
+ if (d === "\\") {
326
+ j += 2;
327
+ continue;
328
+ }
329
+ if (cls) {
330
+ if (d === "]") cls = false;
331
+ } else if (d === "[") cls = true;
332
+ else if (d === "/") break;
333
+ j++;
334
+ }
335
+ if (j < n && src[j] === "/") {
336
+ blank(i + 1, j);
337
+ i = j + 1;
338
+ continue;
339
+ }
340
+ }
341
+ }
114
342
  if (c === '"' || c === "'" || (backtick && c === "`")) {
115
343
  const verbatim = lang === "cs" && src[i - 1] === "@";
116
344
  const raw = c === "`" && lang === "go";
345
+ // W2 (G5): a template literal's `${…}` and an f-string's `{…}` are CODE:
346
+ // `${fn()}` is a call, and `f"{base}/p"` keeps its expression. The
347
+ // string is recorded as segments around each expression, and the
348
+ // expression is masked on its own (a string inside it is still a string).
349
+ const tpl = (c === "`" && !raw) || (lang === "rb" && c !== "'");
350
+ const fstr = lang === "py" && c !== "`" && /^[fF]$|^[rRbB][fF]$|^[fF][rRbB]$/.test(((src[i - 2] || "") + (src[i - 1] || "")).replace(/^[^A-Za-z]/, ""));
117
351
  let j = i + 1;
352
+ let seg = i + 1;
118
353
  while (j < n) {
119
354
  const d = src[j];
120
355
  if (d === "\\" && !verbatim && !raw) {
@@ -132,10 +367,31 @@ function mask(src, lang) {
132
367
  // literal, an apostrophe in a template): stop at the line end so one
133
368
  // bad guess costs one line, not the rest of the file.
134
369
  if (d === "\n" && c !== "`" && !verbatim) break;
370
+ // A Ruby interpolation opens with `#{`, a JS template with `${`.
371
+ if ((tpl && d === (lang === "rb" ? "#" : "$") && src[j + 1] === "{") || (fstr && d === "{" && src[j + 1] !== "{")) {
372
+ const open = tpl ? j + 1 : j;
373
+ const close = closeBrace(src, open);
374
+ if (close < 0) {
375
+ j++;
376
+ continue;
377
+ }
378
+ strings.push({ start: seg, end: j });
379
+ blank(seg, j);
380
+ const inner = mask(src.slice(open + 1, close), lang);
381
+ for (let q = 0; q < inner.masked.length; q++) m[open + 1 + q] = inner.masked[q];
382
+ for (const s of inner.strings) strings.push({ start: open + 1 + s.start, end: open + 1 + s.end });
383
+ j = close + 1;
384
+ seg = j;
385
+ continue;
386
+ }
387
+ if (fstr && d === "{" && src[j + 1] === "{") {
388
+ j += 2;
389
+ continue;
390
+ }
135
391
  j++;
136
392
  }
137
- strings.push({ start: i + 1, end: Math.min(j, n) });
138
- blank(i + 1, j);
393
+ strings.push({ start: seg, end: Math.min(j, n) });
394
+ blank(seg, j);
139
395
  i = j + 1;
140
396
  continue;
141
397
  }
@@ -144,6 +400,26 @@ function mask(src, lang) {
144
400
  return { masked: m.join(""), strings };
145
401
  }
146
402
 
403
+ // The `}` that closes the brace at `open`, skipping quoted text on the way.
404
+ function closeBrace(src, open) {
405
+ let d = 0;
406
+ for (let k = open; k < src.length; k++) {
407
+ const ch = src[k];
408
+ if (ch === '"' || ch === "'" || ch === "`") {
409
+ let q = k + 1;
410
+ while (q < src.length && src[q] !== ch && src[q] !== "\n") q += src[q] === "\\" ? 2 : 1;
411
+ k = q;
412
+ continue;
413
+ }
414
+ if (ch === "{") d++;
415
+ else if (ch === "}") {
416
+ d--;
417
+ if (d === 0) return k;
418
+ }
419
+ }
420
+ return -1;
421
+ }
422
+
147
423
  // COVERAGE (EW1). A heuristic extractor can lose the thread: an opening brace
148
424
  // with no partner makes a declaration run to the end of the file, and a symbol
149
425
  // that hits a cap keeps only part of what it found. Neither is an error — the
@@ -198,32 +474,182 @@ function bodyAfter(m, from, requireArrow) {
198
474
  return null;
199
475
  }
200
476
 
477
+ // ── call arguments (v1.8.2 W1) ──────────────────────────────────────────────
478
+ // The mask blanks string CONTENT but keeps the quotes, so an argument that is
479
+ // one string reads as `" "` in the mask and its text is read back from the
480
+ // original at the recorded span. Each top-level argument becomes one piece:
481
+ // s the string text, when the piece IS one string literal
482
+ // id a bare or dotted identifier (`ordersRouter`, `views.list`, `C::class`)
483
+ // req the specifier of an inline `require("…")`
484
+ // kw the keyword when the piece is `name=value` / `name: value`
485
+ // list the string items of a `[...]` value (`methods=["GET","POST"]`)
486
+ // obj `{ method: "POST", … }` — the string-valued keys of an object literal
487
+ // inc Django: the specifier inside an `include("…")` piece
488
+ // W5 (G6). Kotlin gives a function either a block body or an EXPRESSION body
489
+ // (`fun total() = items.sumOf { it.price }`), and both are real declarations.
490
+ // A member of an interface, or an `abstract fun`, has neither and is not one.
491
+ function ktBody(m, from) {
492
+ const limit = Math.min(m.length, from + 400);
493
+ for (let k = from; k < limit; k++) {
494
+ const ch = m[k];
495
+ if (ch === "{") return { open: k, end: matchPair(m, k, "{", "}") };
496
+ if (ch === "=" && m[k + 1] !== "=") {
497
+ let j = k + 1;
498
+ while (j < m.length && (m[j] === " " || m[j] === "\t" || m[j] === "\r" || m[j] === "\n")) j++;
499
+ if (m[j] === "{") return { open: j, end: matchPair(m, j, "{", "}") };
500
+ const nl = m.indexOf("\n", j);
501
+ return { open: j, end: nl < 0 ? m.length - 1 : nl - 1 };
502
+ }
503
+ if (ch === "\n") {
504
+ let j = k + 1;
505
+ while (j < limit && (m[j] === " " || m[j] === "\t" || m[j] === "\r")) j++;
506
+ // A `:` return type or a `where` clause continues the header; anything
507
+ // else on the next line means this declaration had no body.
508
+ if (m[j] !== ":" && m[j] !== "{" && m[j] !== "=" && m.slice(j, j + 5) !== "where") return null;
509
+ }
510
+ }
511
+ return null;
512
+ }
513
+
514
+ function firstStringIn(src, strAt, a, b) {
515
+ for (let k = a; k < b; k++) {
516
+ const ch = src[k];
517
+ if (ch !== '"' && ch !== "'" && ch !== "`") continue;
518
+ const sp = strAt.get(k + 1);
519
+ if (sp) return src.slice(sp.start, sp.end);
520
+ }
521
+ return null;
522
+ }
523
+
524
+ function stringsIn(src, strAt, a, b) {
525
+ const out = [];
526
+ for (let k = a; k < b; k++) {
527
+ const ch = src[k];
528
+ if (ch !== '"' && ch !== "'" && ch !== "`") continue;
529
+ const sp = strAt.get(k + 1);
530
+ if (sp) {
531
+ out.push(src.slice(sp.start, sp.end));
532
+ k = sp.end;
533
+ }
534
+ }
535
+ return out;
536
+ }
537
+
538
+ const KW_PIECE = /^\s*([A-Za-z_]\w*)\s*(?:=(?![=>])|:(?!:))\s*([\s\S]*)$/;
539
+ const IDENT_PIECE = /^\s*&?\$?((?:[A-Za-z_$][\w$]*\s*(?:\.|->|::)\s*)*[A-Za-z_$][\w$]*)\s*$/;
540
+
541
+ function argPieces(c, open, close) {
542
+ const { src, m, strAt } = c;
543
+ const pieces = [];
544
+ let depth = 0;
545
+ let a = open + 1;
546
+ for (let k = open + 1; k <= close && k < m.length; k++) {
547
+ const ch = m[k];
548
+ const end = k === close;
549
+ if (!end && (ch === "(" || ch === "[" || ch === "{")) depth++;
550
+ else if (!end && (ch === ")" || ch === "]" || ch === "}")) depth--;
551
+ if (!end && !(ch === "," && depth === 0)) continue;
552
+ const text = m.slice(a, k);
553
+ if (text.trim()) pieces.push(piece(c, a, k, text));
554
+ a = k + 1;
555
+ if (pieces.length >= 8) break;
556
+ }
557
+ return pieces;
558
+
559
+ function piece(c, start, end, text) {
560
+ const p = {};
561
+ let body = text;
562
+ let bodyStart = start;
563
+ const kw = KW_PIECE.exec(text);
564
+ if (kw && !/^\s*(new|return|await)\b/.test(kw[2])) {
565
+ p.kw = kw[1];
566
+ body = kw[2];
567
+ bodyStart = start + text.length - kw[2].length;
568
+ }
569
+ const t = body.trim();
570
+ const q = /^\s*(?:[A-Za-z]{1,2}|@)?(["'`])([\s\S]*)\1\s*$/.exec(body);
571
+ if (q && !q[2].includes(q[1])) {
572
+ // ONE string literal. A template literal / f-string with expressions
573
+ // (G5 keeps them as code, so the mask is not blank between the quotes)
574
+ // is rebuilt from its recorded segments, `{}` per expression: `${base}/p`
575
+ // is a suffix, `/orders/${id}` matches a route's parameter segment.
576
+ if (/^\s*$/.test(q[2])) p.s = firstStringIn(src, strAt, bodyStart, end);
577
+ else {
578
+ const segs = c.strings.filter((s) => s.start >= bodyStart && s.end <= end).map((s) => src.slice(s.start, s.end));
579
+ if (segs.length) p.s = segs.join("{}");
580
+ }
581
+ } else if (/^\[[\s\S]*\]$/.test(t)) p.list = stringsIn(src, strAt, bodyStart, end);
582
+ else if (/^\{[\s\S]*\}$/.test(t)) {
583
+ const obj = {};
584
+ const re = /([A-Za-z_]\w*)\s*:\s*(["'`])/g;
585
+ let x;
586
+ while ((x = re.exec(body))) {
587
+ const at = bodyStart + x.index + x[0].length;
588
+ const sp = strAt.get(at);
589
+ if (sp) obj[x[1]] = src.slice(sp.start, sp.end);
590
+ }
591
+ if (Object.keys(obj).length) p.obj = obj;
592
+ } else if (/\+\s*(["'`])[^"'`]*\1\s*$/.test(t)) {
593
+ // `BASE + "/p"` — the TAIL is known, the prefix is not.
594
+ const all = stringsIn(src, strAt, bodyStart, end);
595
+ if (all.length) p.cat = all[all.length - 1];
596
+ } else {
597
+ const req = /^\s*require\s*\(\s*(["'])[^"']*\1\s*\)\s*$/.exec(body);
598
+ if (req) p.req = firstStringIn(src, strAt, bodyStart, end);
599
+ const inc = /^\s*include\s*\(/.exec(body);
600
+ if (inc) p.inc = firstStringIn(src, strAt, bodyStart, end);
601
+ const cls = /\[\s*([A-Za-z_\\][\w\\]*)\s*::\s*class\s*,\s*(["'])[^"']*\2\s*\]/.exec(body);
602
+ if (cls) p.id = cls[1].split("\\").pop() + "." + (firstStringIn(src, strAt, bodyStart, end) || "");
603
+ const id = !p.req && !p.inc && !p.id ? IDENT_PIECE.exec(body) : null;
604
+ if (id && !KW.has(id[1]) && !/^\d/.test(id[1])) p.id = id[1].replace(/\s+/g, "").replace(/->|::/g, ".");
605
+ }
606
+ return p;
607
+ }
608
+ }
609
+
610
+ // What a call's arguments say the RIGHT-HAND assignment target is, when the
611
+ // call is `x = f(...)`. Used for router prefixes (`router = APIRouter(prefix=)`)
612
+ // here, and for instance aliases in W2.
613
+ const ASSIGN_BEFORE = /(?:^|[;{}\n(,:])\s*(?:(?:const|let|var|val|final|public|private|protected|readonly|static|export)\s+)*(?:[A-Za-z_][\w<>\[\],?]*\s+)?((?:\$?[A-Za-z_][\w$]*\s*(?:\.|->)\s*)*\$?[A-Za-z_$][\w$]*)\s*(?::\s*[\w.<>\[\]|?]+\s*)?(?::=|=)\s*(?:new\s+)?$/;
614
+ function assignBefore(m, at) {
615
+ const before = m.slice(Math.max(0, at - 120), at);
616
+ const x = ASSIGN_BEFORE.exec(before);
617
+ if (!x) return null;
618
+ let n = x[1].replace(/\s+/g, "").replace(/->/g, ".").replace(/\$/g, "");
619
+ if (/^(this|self)\./.test(n)) n = "self." + n.replace(/^(this|self)\./, "");
620
+ return KW.has(n) ? null : n;
621
+ }
622
+
201
623
  // ── declarations: brace languages ───────────────────────────────────────────
202
- function braceDefs(src, m, starts, lang, strings) {
624
+ function braceDefs(c, lang) {
625
+ const { src, m, starts } = c;
203
626
  const defs = [];
204
627
  const defParens = new Set();
628
+ // The parens of a route REGISTRATION (`router.get("/p", fn)`): the call
629
+ // stays a call, but its URL is the route's own path, never a URL it reaches.
630
+ const regParens = new Set();
205
631
  const classes = [];
206
632
  const add = (d) => {
207
- if (!d.name || KW.has(d.name)) return;
633
+ if (!d.name || isKw(lang, d.name)) return;
208
634
  d.s = lineOf(starts, d.from);
209
635
  d.e = lineOf(starts, Math.max(d.from, d.to));
210
636
  defs.push(d);
211
637
  };
212
638
  const innermostClass = (pos) => {
213
639
  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;
640
+ for (const cl of classes) if (cl.open < pos && pos < cl.to && (!best || cl.to - cl.open < best.to - best.open)) best = cl;
215
641
  return best;
216
642
  };
217
643
  let x;
218
644
 
219
645
  if (lang === "js" || lang === "ts") {
220
- const CLASS = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:abstract[ \t]+)?class[ \t]+([A-Za-z_$][\w$]*)[^{;]*\{/gm;
646
+ const CLASS = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:abstract[ \t]+)?class[ \t]+([A-Za-z_$][\w$]*)([^{;]*)\{/gm;
221
647
  while ((x = CLASS.exec(m))) {
222
648
  const open = x.index + x[0].length - 1;
223
649
  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);
650
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: !!x[1], bases: basesOf(x[3]) };
651
+ classes.push(cl);
652
+ add(cl);
227
653
  }
228
654
  const FN = /^[ \t]*(export[ \t]+)?(?:default[ \t]+)?(?:async[ \t]+)?function[ \t]*\*?[ \t]*([A-Za-z_$][\w$]*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
229
655
  while ((x = FN.exec(m))) {
@@ -253,28 +679,34 @@ function braceDefs(src, m, starts, lang, strings) {
253
679
  // file has NO symbol and its card is an import list — W9 measured 6 files,
254
680
  // 1 symbol on an Express app. Named `<METHOD> <path>`; the body is the whole
255
681
  // 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;
682
+ // the handler belong to the route. W1: a handler passed BY NAME
683
+ // (`app.get("/p", handler)`) is a route too, when the receiver is a router.
684
+ const ROUTE = /\b([A-Za-z_$][\w$]*)[ \t]*\.[ \t]*(get|post|put|patch|delete|del|all|options|head)[ \t]*\(/g;
685
+ const ROUTER_HEAD = /^(router|app|server|api|routes?|r|fastify|koa|hono|express|v\d+)$/i;
258
686
  while ((x = ROUTE.exec(m))) {
259
687
  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
688
  const close = matchPair(m, paren, "(", ")");
689
+ const pieces = argPieces(c, paren, close);
690
+ const p0 = pieces[0];
691
+ if (!p0 || p0.s == null || p0.kw) continue;
692
+ const routePath = p0.s;
693
+ if (!/^(\/|\*$)/.test(routePath) || /[\r\n]/.test(routePath)) continue;
267
694
  // `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 });
695
+ const fnArg = /=>|\bfunction\b/.test(m.slice(p0 ? paren + 1 : paren, close));
696
+ const namedHandler = !fnArg && pieces.length >= 2 && pieces[pieces.length - 1].id && ROUTER_HEAD.test(x[1]);
697
+ if (!fnArg && !namedHandler) continue;
698
+ regParens.add(paren);
699
+ add({ kind: "route", method: VERB_OF(x[2]), path: routePath, head: x[1], name: "route", from: x.index, to: close, exported: false });
271
700
  }
272
701
  // Members: scanned inside each class body, skipping every member body it
273
702
  // 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) {
703
+ // A member starts at a line start, or right after the `{` / `}` / `;`
704
+ // before it — so `class A extends B { go() { … } }` on one line is a
705
+ // class WITH a member (W2: two fixtures and one real repository had it).
706
+ 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;
707
+ for (const cl of classes) {
708
+ MEMBER.lastIndex = cl.open + 1;
709
+ while ((x = MEMBER.exec(m)) && x.index < cl.to) {
278
710
  const end = x.index + x[0].length;
279
711
  let body = null;
280
712
  let paren = null;
@@ -291,14 +723,14 @@ function braceDefs(src, m, starts, lang, strings) {
291
723
  // A member with no body is an overload signature or an abstract
292
724
  // declaration. Recording it made `NestFactoryStatic.create` three
293
725
  // 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);
726
+ if (!body || body.end >= cl.to || m[body.open] === ";") {
727
+ if (body && body.end < cl.to) MEMBER.lastIndex = Math.max(body.end + 1, end);
296
728
  continue;
297
729
  }
298
730
  const name = x[2].replace(/^#/, "");
299
- if (!KW.has(name)) {
731
+ if (!MEMBER_KW.has(name)) {
300
732
  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 });
733
+ add({ name, qname: `${cl.name}.${name}`, kind: "method", from: x.index, to: body.end, exported: cl.exported && !priv, member: true, owner: cl });
302
734
  }
303
735
  MEMBER.lastIndex = Math.max(body.end + 1, end);
304
736
  }
@@ -327,19 +759,38 @@ function braceDefs(src, m, starts, lang, strings) {
327
759
  exported: /^[A-Z]/.test(x[2]),
328
760
  });
329
761
  }
762
+ // W1: net/http, gin, echo, chi registrations with a handler BY NAME. A Go
763
+ // 1.22 pattern carries its method in the string (`"GET /orders"`).
764
+ const GOROUTE = /\b([A-Za-z_]\w*)[ \t]*\.[ \t]*(HandleFunc|Handle|GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|Any|Get|Post|Put|Patch|Delete|Head|Options)[ \t]*\(/g;
765
+ while ((x = GOROUTE.exec(m))) {
766
+ const paren = x.index + x[0].length - 1;
767
+ const close = matchPair(m, paren, "(", ")");
768
+ const pieces = argPieces(c, paren, close);
769
+ if (pieces.length < 2 || pieces[0].s == null) continue;
770
+ let method = /^(HandleFunc|Handle|Any)$/.test(x[2]) ? "ANY" : x[2].toUpperCase();
771
+ let routePath = pieces[0].s;
772
+ const pat = /^([A-Z]+)\s+(\/\S*)$/.exec(routePath);
773
+ if (pat) {
774
+ method = pat[1];
775
+ routePath = pat[2];
776
+ }
777
+ if (!routePath.startsWith("/")) continue;
778
+ regParens.add(paren);
779
+ add({ kind: "route", method, path: routePath, head: x[1], name: "route", from: x.index, to: close, exported: false });
780
+ }
330
781
  } 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;
782
+ 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
783
  while ((x = CLASS.exec(m))) {
333
784
  const open = x.index + x[0].length - 1;
334
785
  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);
786
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to, open, exported: /public/.test(x[1]), bases: basesOf(x[3]) };
787
+ classes.push(cl);
788
+ add(cl);
338
789
  }
339
790
  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
791
  while ((x = METHOD.exec(m))) {
341
792
  const typeWord = x[2].split(/[<.\[]/)[0];
342
- if (KW.has(typeWord) || KW.has(x[3])) continue;
793
+ if (KW.has(typeWord) || isKw(lang, x[3])) continue;
343
794
  const paren = x.index + x[0].length - 1;
344
795
  const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
345
796
  if (!body || (m[body.open] === ";" && !/abstract|extern|native/.test(x[1]))) continue;
@@ -352,6 +803,7 @@ function braceDefs(src, m, starts, lang, strings) {
352
803
  from: x.index,
353
804
  to: body.end,
354
805
  exported: /public/.test(x[1]),
806
+ owner,
355
807
  });
356
808
  }
357
809
  const CTOR = /^[ \t]*(?:(?:public|private|protected|internal)[ \t]+)([A-Z]\w*)[ \t]*\(/gm;
@@ -362,16 +814,19 @@ function braceDefs(src, m, starts, lang, strings) {
362
814
  const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
363
815
  if (!body) continue;
364
816
  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]) });
817
+ add({ name: x[1], qname: `${owner.name}.${x[1]}`, kind: "method", from: x.index, to: body.end, exported: /public/.test(x[0]), owner });
366
818
  }
367
819
  } else if (lang === "php") {
368
- const CLASS = /^[ \t]*(?:(?:abstract|final|readonly)[ \t]+)*(?:class|interface|trait|enum)[ \t]+([A-Za-z_]\w*)[^{;]*\{/gm;
820
+ const CLASS = /^[ \t]*(?:(?:abstract|final|readonly)[ \t]+)*(?:class|interface|trait|enum)[ \t]+([A-Za-z_]\w*)([^{;]*)\{/gm;
369
821
  while ((x = CLASS.exec(m))) {
370
822
  const open = x.index + x[0].length - 1;
371
823
  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);
824
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to, open, exported: true, bases: basesOf(x[2]) };
825
+ // `use Trait;` inside the body is a base too.
826
+ for (const u of m.slice(open, to).matchAll(/^[ \t]*use[ \t]+([A-Za-z_\\][\w\\, \t]*);/gm))
827
+ for (const t of u[1].split(",")) if (t.trim()) cl.bases.push(t.trim().split("\\").pop());
828
+ classes.push(cl);
829
+ add(cl);
375
830
  }
376
831
  const FUNC = /^[ \t]*((?:(?:public|private|protected|static|abstract|final)[ \t]+)*)function[ \t]+&?([A-Za-z_]\w*)[ \t]*\(/gm;
377
832
  while ((x = FUNC.exec(m))) {
@@ -387,6 +842,186 @@ function braceDefs(src, m, starts, lang, strings) {
387
842
  from: x.index,
388
843
  to: body.end,
389
844
  exported: !/private|protected/.test(x[1]),
845
+ owner,
846
+ });
847
+ }
848
+ // Laravel: `Route::get('/p', [OrdersController::class, 'index'])`.
849
+ const LARAVEL = /\bRoute[ \t]*::[ \t]*(get|post|put|patch|delete|any|options)[ \t]*\(/g;
850
+ while ((x = LARAVEL.exec(m))) {
851
+ const paren = x.index + x[0].length - 1;
852
+ const close = matchPair(m, paren, "(", ")");
853
+ const pieces = argPieces(c, paren, close);
854
+ if (!pieces[0] || pieces[0].s == null) continue;
855
+ regParens.add(paren);
856
+ add({ kind: "route", method: x[1] === "any" ? "ANY" : VERB_OF(x[1]), path: pieces[0].s, head: "Route", name: "route", from: x.index, to: close, exported: false });
857
+ }
858
+ } else if (lang === "rs") {
859
+ // W5 (G6). A Rust type is a `struct`, `enum`, `trait` or `union`; its
860
+ // methods live in an `impl` block somewhere else in the file, so the impl
861
+ // block — not the type — is what owns a method.
862
+ const TYPE = /^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?(?:struct|enum|trait|union)[ \t]+([A-Za-z_]\w*)/gm;
863
+ while ((x = TYPE.exec(m))) {
864
+ const brace = m.indexOf("{", x.index + x[0].length);
865
+ const semi = m.indexOf(";", x.index + x[0].length);
866
+ const unit = semi >= 0 && (brace < 0 || semi < brace);
867
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to: unit ? semi : brace < 0 ? x.index : matchPair(m, brace, "{", "}"), exported: /pub/.test(x[0]), bases: [] };
868
+ add(cl);
869
+ // A `trait` body owns its DEFAULT methods, which is what makes
870
+ // `self.ok(…)` on an implementing struct an inherited call.
871
+ if (!unit && brace >= 0) classes.push({ ...cl, open: brace });
872
+ }
873
+ // `impl Trait for Type {` and `impl<T> Type<T> {` — the owner is the TYPE,
874
+ // which is the last word of the tail.
875
+ const IMPL = /^[ \t]*(?:unsafe[ \t]+)?impl(?:[ \t]*<[^>\n]*>)?[ \t]+([^{\n]+?)[ \t]*\{/gm;
876
+ while ((x = IMPL.exec(m))) {
877
+ const tail = x[1].split(/\bfor\b/).pop();
878
+ const mm = /([A-Za-z_]\w*)/.exec(tail.replace(/<[^>]*>/g, "").replace(/'\w+/g, ""));
879
+ if (!mm) continue;
880
+ const open = x.index + x[0].length - 1;
881
+ classes.push({ name: mm[1], qname: mm[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: false, bases: [] });
882
+ // `impl Trait for Type` is Rust's inheritance: the type gains the
883
+ // trait's default methods, so the trait is a BASE of the type.
884
+ const tr = /\bfor\b/.test(x[1]) ? /([A-Za-z_]\w*)/.exec(x[1].split(/\bfor\b/)[0].replace(/<[^>]*>/g, "").replace(/'\w+/g, "")) : null;
885
+ if (tr) for (const d of defs) if (d.kind === "class" && d.name === mm[1] && d.bases) d.bases.push(tr[1]);
886
+ }
887
+ const FN = /(?:^|[{};])[ \t]*(?:#\[[^\]]*\][ \t\r\n]*)*((?:(?:pub(?:\([^)]*\))?|async|unsafe|const|extern[ \t]+"[^"\n]*")[ \t]+)*)fn[ \t]+([A-Za-z_]\w*)[ \t]*(?:<[^>\n]*>)?[ \t]*\(/gm;
888
+ while ((x = FN.exec(m))) {
889
+ const paren = x.index + x[0].length - 1;
890
+ const body = bodyAfter(m, matchPair(m, paren, "(", ")") + 1, false);
891
+ defParens.add(paren);
892
+ // A trait method with no body is a signature, not a symbol.
893
+ if (!body || m[body.open] === ";") continue;
894
+ const owner = innermostClass(x.index);
895
+ add({
896
+ name: x[2],
897
+ qname: owner ? `${owner.name}.${x[2]}` : x[2],
898
+ kind: owner ? "method" : "function",
899
+ from: x.index,
900
+ to: body.end,
901
+ exported: /\bpub\b/.test(x[1]),
902
+ owner,
903
+ });
904
+ }
905
+ // axum / actix builder registration: `.route("/orders", get(list))`.
906
+ const RSROUTE = /\.[ \t]*route[ \t]*\(/g;
907
+ while ((x = RSROUTE.exec(m))) {
908
+ const paren = x.index + x[0].length - 1;
909
+ const close = matchPair(m, paren, "(", ")");
910
+ const pieces = argPieces(c, paren, close);
911
+ if (!pieces[0] || pieces[0].s == null || !pieces[0].s.startsWith("/")) continue;
912
+ const verbs = [...m.slice(paren, close).matchAll(/\b(get|post|put|patch|delete|head|options)[ \t]*\(/g)].map((v) => v[1]);
913
+ if (!verbs.length) continue;
914
+ regParens.add(paren);
915
+ for (const v of verbs) add({ kind: "route", method: VERB_OF(v), path: pieces[0].s, head: "route", name: "route", from: x.index, to: close, exported: false });
916
+ }
917
+ } else if (lang === "kt") {
918
+ const CLASS = /^[ \t]*((?:(?:public|private|protected|internal|open|abstract|sealed|data|enum|inner|annotation|value)[ \t]+)*)(?:class|object|interface)[ \t]+([A-Za-z_]\w*)([^{\n]*)\{/gm;
919
+ while ((x = CLASS.exec(m))) {
920
+ const open = x.index + x[0].length - 1;
921
+ const cl = { name: x[2], qname: x[2], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: !/private|internal/.test(x[1]), bases: basesOf(x[3].replace(/\([^)]*\)/g, "")) };
922
+ classes.push(cl);
923
+ add(cl);
924
+ }
925
+ // `fun list(): List<Order>` · `fun Order.label()` (an extension) · an
926
+ // expression body (`fun n() = x`) is as real a declaration as a block one.
927
+ const FUN = /^[ \t]*((?:(?:public|private|protected|internal|open|override|suspend|inline|operator|abstract|external|tailrec|infix)[ \t]+)*)fun[ \t]*(?:<[^>\n]*>[ \t]*)?(?:([A-Za-z_]\w*)[ \t]*\.[ \t]*)?([A-Za-z_]\w*|`[^`\n]+`)[ \t]*\(/gm;
928
+ while ((x = FUN.exec(m))) {
929
+ const paren = x.index + x[0].length - 1;
930
+ const body = ktBody(m, matchPair(m, paren, "(", ")") + 1);
931
+ defParens.add(paren);
932
+ if (!body) continue;
933
+ const name = x[3].replace(/^`|`$/g, "");
934
+ const owner = x[2] ? { name: x[2] } : innermostClass(x.index);
935
+ add({
936
+ name,
937
+ qname: owner ? `${owner.name}.${name}` : name,
938
+ kind: owner ? "method" : "function",
939
+ from: x.index,
940
+ to: body.end,
941
+ exported: !/private|internal/.test(x[1]),
942
+ owner: x[2] ? null : owner,
943
+ });
944
+ }
945
+ // Ktor: `get("/orders") { … }`, nested inside `route("/api") { … }`.
946
+ const GROUPS = [];
947
+ const KROUTE = /(?:^|[^\w.])route[ \t]*\(/g;
948
+ while ((x = KROUTE.exec(m))) {
949
+ const paren = x.index + x[0].length - 1;
950
+ const close = matchPair(m, paren, "(", ")");
951
+ const pieces = argPieces(c, paren, close);
952
+ const brace = m.indexOf("{", close);
953
+ if (!pieces[0] || pieces[0].s == null || brace < 0 || brace > close + 4) continue;
954
+ GROUPS.push({ path: pieces[0].s, open: brace, to: matchPair(m, brace, "{", "}") });
955
+ regParens.add(paren);
956
+ }
957
+ const KVERB = /(?:^|[^\w.])(get|post|put|patch|delete|head|options)[ \t]*\(/g;
958
+ while ((x = KVERB.exec(m))) {
959
+ const paren = x.index + x[0].length - 1;
960
+ const close = matchPair(m, paren, "(", ")");
961
+ const pieces = argPieces(c, paren, close);
962
+ const brace = m.indexOf("{", close);
963
+ if (!pieces[0] || pieces[0].s == null || !pieces[0].s.startsWith("/") || brace < 0 || brace > close + 4) continue;
964
+ let pre = "";
965
+ for (const g of GROUPS) if (g.open < x.index && x.index < g.to) pre = joinPath(pre, g.path);
966
+ regParens.add(paren);
967
+ add({ kind: "route", method: VERB_OF(x[1]), path: joinPath(pre, pieces[0].s), head: "route", name: "route", from: x.index, to: matchPair(m, brace, "{", "}"), exported: false });
968
+ }
969
+ } else if (lang === "c") {
970
+ const CLASS = /^[ \t]*(?:template[ \t]*<[^>\n]*>[ \t\r\n]*)?(?:class|struct)[ \t]+([A-Za-z_]\w*)([^{;\n]*)\{/gm;
971
+ while ((x = CLASS.exec(m))) {
972
+ const open = x.index + x[0].length - 1;
973
+ const cl = { name: x[1], qname: x[1], kind: "class", from: x.index, to: matchPair(m, open, "{", "}"), open, exported: true, bases: basesOf(x[2].replace(/\b(public|private|protected|virtual)\b/g, "")) };
974
+ classes.push(cl);
975
+ add(cl);
976
+ }
977
+ // A MACRO at column 0 with a block body is a definition the preprocessor
978
+ // writes: `TEST_F(NonMutatingTest, Equal) { … }`. Without this rung every
979
+ // such body belongs to the module symbol, which then passes the call cap
980
+ // and reports the whole file `partial` — measured on abseil-cpp, that was
981
+ // most of the gap, and every one of those files was a test file.
982
+ const MACRODEF = /^([A-Z][A-Z0-9_]{2,})[ \t]*\(/gm;
983
+ while ((x = MACRODEF.exec(m))) {
984
+ const paren = x.index + x[0].length - 1;
985
+ const close = matchPair(m, paren, "(", ")");
986
+ if (close < 0) continue;
987
+ const body = bodyAfter(m, close + 1, false);
988
+ if (!body || m[body.open] !== "{") continue;
989
+ const ids = argPieces(c, paren, close)
990
+ .filter((a) => !a.kw && a.id)
991
+ .map((a) => a.id)
992
+ .filter((sid) => /^[A-Za-z_]\w*$/.test(sid));
993
+ const name = ids.length >= 2 ? ids[1] : ids.length === 1 ? ids[0] : x[1];
994
+ defParens.add(paren);
995
+ add({ name, qname: ids.length >= 2 ? `${ids[0]}.${name}` : name, kind: ids.length >= 2 ? "method" : "function", from: x.index, to: body.end, exported: true });
996
+ }
997
+ // A definition is `<type> name(args) {`, or `Type::name(args) {` for a
998
+ // member defined outside its class. A DECLARATION ends in `;` and is not
999
+ // a symbol — the header file would otherwise duplicate every function.
1000
+ const FUNC = /(?:^|[{};])[ \t]*((?:(?:static|inline|extern|virtual|explicit|constexpr|friend|const|unsigned|signed)[ \t]+)*)([A-Za-z_][\w:<>,*&\[\] \t]*?[ \t*&])?(?:([A-Za-z_]\w*)[ \t]*::[ \t]*)?(~?[A-Za-z_]\w*)[ \t]*\(/gm;
1001
+ while ((x = FUNC.exec(m))) {
1002
+ const name = x[4];
1003
+ if (isKw(lang, name) || MEMBER_KW.has(name)) continue;
1004
+ if (!x[2] && !x[3] && !/^~/.test(name)) continue; // a plain call, not a definition
1005
+ // `return ok(body);` looks like `<type> <name>(` — the "type" is a
1006
+ // keyword, so it is a CALL, and it must stay one. The paren is claimed
1007
+ // for a declaration only once the body proves it is a definition.
1008
+ const typeWord = (x[2] || "").trim().split(/[<:*&[\s]/)[0];
1009
+ if (typeWord && (KW.has(typeWord) || isKw(lang, typeWord))) continue;
1010
+ const paren = x.index + x[0].length - 1;
1011
+ const close = matchPair(m, paren, "(", ")");
1012
+ if (close < 0) continue;
1013
+ const body = bodyAfter(m, close + 1, false);
1014
+ if (!body || m[body.open] === ";") continue;
1015
+ defParens.add(paren);
1016
+ const owner = x[3] ? { name: x[3] } : innermostClass(x.index);
1017
+ add({
1018
+ name,
1019
+ qname: owner ? `${owner.name}.${name}` : name,
1020
+ kind: owner ? "method" : "function",
1021
+ from: x.index,
1022
+ to: body.end,
1023
+ exported: !/\bstatic\b/.test(x[1]),
1024
+ owner: x[3] ? null : owner,
390
1025
  });
391
1026
  }
392
1027
  }
@@ -411,21 +1046,35 @@ function braceDefs(src, m, starts, lang, strings) {
411
1046
  } else exportsSet.add(me[1]);
412
1047
  }
413
1048
  }
414
- return { defs, defParens, exportsSet };
1049
+ return { defs, defParens, regParens, exportsSet };
1050
+ }
1051
+
1052
+ // `class X extends A implements B, C` / `class X : A, IB` → ["A", "B", "C"].
1053
+ // Recorded for W2 (inherited members); harmless before it.
1054
+ function basesOf(tail) {
1055
+ const out = [];
1056
+ if (!tail) return out;
1057
+ const t = tail.replace(/<[^>]*>/g, "");
1058
+ for (const mm of t.matchAll(/(?:\bextends\b|\bimplements\b|:)\s*((?:[A-Za-z_$][\w$.\\]*\s*,\s*)*[A-Za-z_$][\w$.\\]*)/g))
1059
+ for (const b of mm[1].split(","))
1060
+ if (/^[A-Za-z_$][\w$.\\]*$/.test(b.trim()) && !KW.has(b.trim())) out.push(b.trim().split(/[.\\]/).pop());
1061
+ return [...new Set(out)].slice(0, 8);
415
1062
  }
416
1063
 
417
1064
  // ── declarations: Python (indentation) ──────────────────────────────────────
418
- function pyDefs(src, m, starts) {
1065
+ function pyDefs(c) {
1066
+ const { src, m, starts, strAt } = c;
419
1067
  const lines = m.split("\n");
420
1068
  const defs = [];
421
1069
  const defParens = new Set();
1070
+ const regParens = new Set();
422
1071
  const stack = [];
423
1072
  const width = (s) => s.replace(/\t/g, " ").length;
424
1073
  for (let i = 0; i < lines.length; i++) {
425
1074
  const L = lines[i];
426
1075
  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;
1076
+ const cl = d ? null : /^([ \t]*)class[ \t]+([A-Za-z_]\w*)[ \t]*(?:\(([^)]*)\))?/.exec(L);
1077
+ const hit = d || cl;
429
1078
  if (!hit) continue;
430
1079
  const indent = width(hit[1]);
431
1080
  while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
@@ -434,6 +1083,9 @@ function pyDefs(src, m, starts) {
434
1083
  const paren = starts[i] + hit[0].length - 1;
435
1084
  defParens.add(paren);
436
1085
  header = lineOf(starts, matchPair(m, paren, "(", ")")) - 1;
1086
+ } else if (cl[3] !== undefined) {
1087
+ // `class X(Base):` is a declaration, not a call to X.
1088
+ defParens.add(starts[i] + L.indexOf("("));
437
1089
  }
438
1090
  let end = header;
439
1091
  for (let k = header + 1; k < lines.length; k++) {
@@ -445,17 +1097,237 @@ function pyDefs(src, m, starts) {
445
1097
  const chain = [];
446
1098
  for (let s = stack.length - 1; s >= 0 && stack[s].kind === "class"; s--) chain.unshift(stack[s].name);
447
1099
  const name = hit[2];
1100
+ // Decorators are the lines directly above, at the same indent, starting `@`.
1101
+ const decos = [];
1102
+ for (let k = i - 1; k >= 0; k--) {
1103
+ const dm = /^([ \t]*)@([A-Za-z_][\w.]*)[ \t]*(\()?/.exec(lines[k]);
1104
+ if (!dm || width(dm[1]) !== indent) break;
1105
+ const deco = { name: dm[2] };
1106
+ if (dm[3]) {
1107
+ const open = starts[k] + dm[0].length - 1;
1108
+ deco.args = argPieces(c, open, matchPair(m, open, "(", ")"));
1109
+ // `@router.get("/p")` is a route, never a URL the module reaches.
1110
+ regParens.add(open);
1111
+ }
1112
+ decos.unshift(deco);
1113
+ }
448
1114
  defs.push({
449
1115
  name,
450
1116
  qname: [...chain, name].join("."),
451
1117
  kind: d ? (chain.length ? "method" : "function") : "class",
452
1118
  s: i + 1,
453
1119
  e: end + 1,
1120
+ from: starts[i],
454
1121
  exported: !name.startsWith("_"),
1122
+ ...(decos.length ? { decos } : {}),
1123
+ ...(cl && cl[3] ? { bases: cl[3].split(",").map((b) => b.trim().split(".").pop()).filter((b) => /^[A-Za-z_]\w*$/.test(b)) } : {}),
455
1124
  });
456
1125
  stack.push({ indent, name, kind: d ? "function" : "class" });
457
1126
  }
458
- return { defs, defParens };
1127
+ // Django `urlpatterns`: `path("p/", views.x)` / `re_path` / `url(...)` — a
1128
+ // route with its handler by name, or a mount when the target is `include()`.
1129
+ let x;
1130
+ const DJ = /\b(path|re_path|url)[ \t]*\(/g;
1131
+ while ((x = DJ.exec(m))) {
1132
+ const paren = x.index + x[0].length - 1;
1133
+ if (defParens.has(paren)) continue;
1134
+ const close = matchPair(m, paren, "(", ")");
1135
+ const pieces = argPieces(c, paren, close);
1136
+ if (!pieces[0] || pieces[0].s == null || !pieces[1] || pieces[1].inc) continue;
1137
+ if (!pieces[1].id) continue;
1138
+ const p = pieces[0].s.replace(/^\^/, "").replace(/\$$/, "");
1139
+ regParens.add(paren);
1140
+ defs.push({ kind: "route", method: "ANY", path: p.startsWith("/") ? p : "/" + p, head: x[1], name: "route", from: x.index, to: close, s: lineOf(starts, x.index), e: lineOf(starts, close), exported: false });
1141
+ }
1142
+ void src;
1143
+ void strAt;
1144
+ return { defs, defParens, regParens };
1145
+ }
1146
+
1147
+ // ── declarations: Ruby (keyword depth) ──────────────────────────────────────
1148
+ // W5 (G6). Ruby has no braces around a body: every `class`, `module`, `def`,
1149
+ // `if`, `while`, `case`, `begin` and every trailing `do` is closed by one
1150
+ // `end`. So the scanner counts keyword depth, one line at a time, and a
1151
+ // declaration's body runs from its own line to the `end` that pops it.
1152
+ //
1153
+ // The MODIFIER form (`save if valid?`) must never open a block, so `if`,
1154
+ // `unless`, `while` and `until` count only as the FIRST word of a statement —
1155
+ // which is the one place a real block can start.
1156
+ const RB_OPEN_FIRST = /^(class|module|def|if|unless|while|until|for|begin|case)\b/;
1157
+ const RB_DO_TAIL = /(?:^|[)\s])do(?:[ \t]*\|[^|\n]*\|)?[ \t]*$/;
1158
+ const RB_ROUTE = /^(get|post|put|patch|delete|head|options|match)[ \t]+(['"])([^'"\n]+)\2/;
1159
+
1160
+ function rbDefs(c) {
1161
+ const { m, src, starts } = c;
1162
+ const lines = m.split("\n");
1163
+ // The mask blanks string CONTENT, so a route path is read back from the
1164
+ // original bytes — the same rule `argPieces` follows everywhere else.
1165
+ const rawLines = src.split("\n");
1166
+ const defs = [];
1167
+ const defParens = new Set();
1168
+ const regParens = new Set();
1169
+ const stack = [];
1170
+ const classOf = () => {
1171
+ for (let k = stack.length - 1; k >= 0; k--) if (stack[k].kind === "class" || stack[k].kind === "module") return stack[k];
1172
+ return null;
1173
+ };
1174
+ for (let i = 0; i < lines.length; i++) {
1175
+ const t = lines[i].trim();
1176
+ if (!t) continue;
1177
+ const ln = i + 1;
1178
+ // `private` with no argument hides every method below it in this body.
1179
+ if (/^(private|protected)[ \t]*$/.test(t)) {
1180
+ const owner = classOf();
1181
+ if (owner) owner.private = true;
1182
+ continue;
1183
+ }
1184
+ let decl = null;
1185
+ let mm;
1186
+ if ((mm = /^class[ \t]+([A-Z]\w*(?:::\w+)*)(?:[ \t]*<[ \t]*([\w:]+))?/.exec(t))) {
1187
+ decl = { kind: "class", name: mm[1].split("::").pop(), bases: mm[2] ? [mm[2].split("::").pop()] : [] };
1188
+ } else if ((mm = /^module[ \t]+([A-Z]\w*(?:::\w+)*)/.exec(t))) {
1189
+ decl = { kind: "module", name: mm[1].split("::").pop(), bases: [] };
1190
+ } else if ((mm = /^def[ \t]+(self\.)?([A-Za-z_]\w*[?!=]?)/.exec(t))) {
1191
+ decl = { kind: "def", name: mm[2] };
1192
+ const p = lines[i].indexOf("(", lines[i].indexOf(mm[0]));
1193
+ if (p >= 0) defParens.add(starts[i] + p);
1194
+ } else if (RB_ROUTE.test(t) && (mm = RB_ROUTE.exec(rawLines[i].trim()))) {
1195
+ // Rails `get "/orders/:id", to: "orders#show"` in `config/routes.rb`.
1196
+ // The path is what a request URL reaches; the `to:` string names a
1197
+ // controller action in ANOTHER file, so it is not a local edge.
1198
+ const p = mm[3].startsWith("/") ? mm[3] : "/" + mm[3];
1199
+ const open = lines[i].indexOf("(");
1200
+ if (open >= 0 && open < lines[i].indexOf(mm[3])) regParens.add(starts[i] + open);
1201
+ defs.push({
1202
+ kind: "route",
1203
+ method: mm[1] === "match" ? "ANY" : VERB_OF(mm[1]),
1204
+ path: p,
1205
+ head: "routes",
1206
+ name: "route",
1207
+ from: starts[i],
1208
+ to: starts[i] + lines[i].length,
1209
+ s: ln,
1210
+ e: ln,
1211
+ exported: false,
1212
+ });
1213
+ }
1214
+ const firstKw = RB_OPEN_FIRST.exec(t);
1215
+ let opens = firstKw ? 1 : 0;
1216
+ // `while x do` and `for a in b do` are ONE block, not two: the trailing
1217
+ // `do` belongs to the loop keyword that already opened it.
1218
+ if (RB_DO_TAIL.test(t) && !(firstKw && /^(while|until|for)$/.test(firstKw[1]))) opens++;
1219
+ const ends = (t.match(/\bend\b/g) || []).length;
1220
+ for (let k = 0; k < opens; k++) {
1221
+ if (k === 0 && decl) stack.push({ ...decl, line: ln, private: false });
1222
+ else stack.push({ kind: "_" });
1223
+ }
1224
+ for (let k = 0; k < ends; k++) {
1225
+ const fr = stack.pop();
1226
+ if (!fr || fr.kind === "_") continue;
1227
+ const owner = classOf();
1228
+ const name = fr.name;
1229
+ if (fr.kind === "def") {
1230
+ defs.push({
1231
+ name,
1232
+ qname: owner ? `${owner.name}.${name}` : name,
1233
+ kind: owner ? "method" : "function",
1234
+ s: fr.line,
1235
+ e: ln,
1236
+ from: starts[fr.line - 1] || 0,
1237
+ exported: !(owner && owner.private) && !name.startsWith("_"),
1238
+ });
1239
+ } else {
1240
+ defs.push({
1241
+ name,
1242
+ qname: name,
1243
+ kind: "class",
1244
+ s: fr.line,
1245
+ e: ln,
1246
+ from: starts[fr.line - 1] || 0,
1247
+ exported: true,
1248
+ ...(fr.bases && fr.bases.length ? { bases: fr.bases } : {}),
1249
+ });
1250
+ }
1251
+ }
1252
+ }
1253
+ defs.sort((a, b) => a.from - b.from);
1254
+ return { defs, defParens, regParens };
1255
+ }
1256
+
1257
+ // ── decorators (brace languages) ────────────────────────────────────────────
1258
+ // `@Get(":id")` / `@GetMapping("/p")` / `[HttpGet("{id}")]` / `#[Route('/p')]`
1259
+ // belong to the NEXT declaration that starts after them. A class-level
1260
+ // `@Controller("orders")` / `@RequestMapping("/orders")` / `[Route("api/x")]`
1261
+ // is the prefix every route in that class carries.
1262
+ function braceDecorators(c, lang, defs) {
1263
+ const { m } = c;
1264
+ const marks = [];
1265
+ let x;
1266
+ if (lang === "js" || lang === "ts" || lang === "java" || lang === "kt") {
1267
+ const RE = /(?:^|[\s;}])@([A-Za-z_$][\w$.]*)[ \t]*(\()?/g;
1268
+ while ((x = RE.exec(m))) marks.push({ pos: x.index + x[0].indexOf("@"), name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1269
+ } else if (lang === "rs") {
1270
+ // actix and rocket put the route on the function: `#[get("/orders")]`.
1271
+ const RE = /#\[[ \t]*(get|post|put|patch|delete|head|options|route)[ \t]*(\()?/g;
1272
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1273
+ } else if (lang === "cs") {
1274
+ const RE = /^[ \t]*\[[ \t]*(HttpGet|HttpPost|HttpPut|HttpPatch|HttpDelete|HttpHead|HttpOptions|Route|ApiController)[ \t]*(\()?/gm;
1275
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1276
+ } else if (lang === "php") {
1277
+ const RE = /#\[[ \t]*(Route|Get|Post|Put|Patch|Delete)[ \t]*(\()?/g;
1278
+ while ((x = RE.exec(m))) marks.push({ pos: x.index, name: x[1], paren: x[2] ? x.index + x[0].length - 1 : null });
1279
+ }
1280
+ if (!marks.length) return;
1281
+ const targets = defs.filter((d) => d.kind !== "route").sort((a, b) => a.from - b.from);
1282
+ for (const mk of marks) {
1283
+ const deco = { name: mk.name };
1284
+ if (mk.paren != null) deco.args = argPieces(c, mk.paren, matchPair(m, mk.paren, "(", ")"));
1285
+ // The nearest declaration that starts after the mark, and not further than
1286
+ // a screen away — a stray `@` in a JSDoc that the mask kept is not a
1287
+ // decorator of the next function.
1288
+ let best = null;
1289
+ for (const d of targets) if (d.from > mk.pos && (!best || d.from < best.from)) best = d;
1290
+ if (!best || best.from - mk.pos > 600) continue;
1291
+ (best.decos || (best.decos = [])).push(deco);
1292
+ }
1293
+ }
1294
+
1295
+ // One decorator → `{ prefix }` (class level) or `{ routes: [{method, path}] }`.
1296
+ function decoRoute(deco, isClass) {
1297
+ const last = String(deco.name).split(".").pop();
1298
+ const args = deco.args || [];
1299
+ const pos = args.filter((a) => !a.kw);
1300
+ const kw = {};
1301
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1302
+ const str = (a) => (a && a.s != null ? a.s : null);
1303
+ const first = str(pos[0]) != null ? str(pos[0]) : str(kw.path) != null ? str(kw.path) : str(kw.value) != null ? str(kw.value) : str(kw.template) != null ? str(kw.template) : null;
1304
+ const norm = (p) => {
1305
+ let s = String(p == null ? "" : p).trim();
1306
+ if (s && !s.startsWith("/")) s = "/" + s;
1307
+ return s;
1308
+ };
1309
+ if (isClass) {
1310
+ if (/^(Controller|RequestMapping|Route|Blueprint)$/.test(last)) return { prefix: norm(first != null ? first : pos[0] && pos[0].obj && pos[0].obj.path) };
1311
+ return null;
1312
+ }
1313
+ if (first == null && pos[0] && pos[0].obj && pos[0].obj.path) return isClass ? { prefix: norm(pos[0].obj.path) } : null;
1314
+ const NEST = /^(Get|Post|Put|Patch|Delete|Head|Options|All)$/;
1315
+ const PY = /^(get|post|put|patch|delete|head|options)$/;
1316
+ const SPRING = /^(Get|Post|Put|Patch|Delete)Mapping$/;
1317
+ const CS = /^Http(Get|Post|Put|Patch|Delete|Head|Options)$/;
1318
+ let methods = null;
1319
+ if (NEST.test(last)) methods = [last === "All" ? "ANY" : last.toUpperCase()];
1320
+ else if (PY.test(last)) methods = [last.toUpperCase()];
1321
+ else if (SPRING.test(last)) methods = [SPRING.exec(last)[1].toUpperCase()];
1322
+ else if (CS.test(last)) methods = [CS.exec(last)[1].toUpperCase()];
1323
+ else if (/^(route|api_route|Route|RequestMapping)$/.test(last)) {
1324
+ const list = (kw.methods && kw.methods.list) || (kw.method && kw.method.list) || null;
1325
+ if (list && list.length) methods = list.map((v) => v.toUpperCase());
1326
+ else if (kw.method && kw.method.id) methods = [kw.method.id.split(".").pop().toUpperCase()];
1327
+ else methods = ["ANY"];
1328
+ }
1329
+ if (!methods) return null;
1330
+ return { routes: methods.map((method) => ({ method, path: norm(first) })) };
459
1331
  }
460
1332
 
461
1333
  // ── calls ───────────────────────────────────────────────────────────────────
@@ -464,20 +1336,36 @@ const CALL = /(?:\bnew[ \t]+)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)
464
1336
  function normCall(name) {
465
1337
  let n = String(name).replace(/[ \t]*(?:\?\.|->|::)[ \t]*/g, ".").replace(/[ \t]+/g, "");
466
1338
  let self = false;
1339
+ let sup = false;
467
1340
  const m = /^(?:\$this|this|self|static|cls)\.(.+)$/.exec(n);
468
1341
  if (m) {
469
1342
  n = m[1];
470
1343
  self = true;
1344
+ } else {
1345
+ // W2 (G3): `super.m()` / `parent::m()` / `base.m()` is a self call that
1346
+ // starts ONE level up the base chain.
1347
+ const sm = /^(?:super|parent|base)\.(.+)$/.exec(n);
1348
+ if (sm) {
1349
+ n = sm[1];
1350
+ self = true;
1351
+ sup = true;
1352
+ }
471
1353
  }
472
1354
  n = n.replace(/^\$/, "").replace(/\.\$/g, ".");
473
1355
  const last = n.split(".").pop();
474
1356
  // A keyword after a dot is a METHOD name — `router.delete(`, `promise.catch(` —
475
1357
  // and the arguments inside it (a middleware passed by name) are still scanned.
476
1358
  if (!last || (KW.has(last) && !n.includes(".")) || /^\d/.test(last)) return null;
477
- return { name: n, self };
1359
+ return { name: n, self, ...(sup ? { sup: true } : {}) };
478
1360
  }
479
1361
 
480
- function scanCalls(m, starts, defParens) {
1362
+ // Which calls carry arguments worth keeping: an HTTP-shaped call (its URL), a
1363
+ // mount, a router constructor (its prefix), or an assignment target. Every
1364
+ // other call keeps only its name and line, as before.
1365
+ const ARGS_WORTH = /(^|\.)(get|post|put|patch|delete|del|head|options|fetch|axios|request|use|register|include_router|register_blueprint|mount|path|re_path|url|include|NewRequest|Get|Post|Head|APIRouter|Blueprint|Router|Route|json|perform|GetAsync|PostAsync|PutAsync|DeleteAsync|PatchAsync|SendAsync|open|route|add_url_rule|Group|Mount|Handle|HandleFunc)$/;
1366
+
1367
+ function scanCalls(c, defParens, regParens) {
1368
+ const { m, starts } = c;
481
1369
  const out = [];
482
1370
  let x;
483
1371
  CALL.lastIndex = 0;
@@ -488,13 +1376,48 @@ function scanCalls(m, starts, defParens) {
488
1376
  // declaration name, are not calls.
489
1377
  const before = m.slice(Math.max(0, x.index - 12), x.index);
490
1378
  if (/\b(function|def|func|fn)[ \t]*\*?[ \t]*$/.test(before)) continue;
491
- const c = normCall(x[1]);
492
- if (!c) continue;
1379
+ const cc = normCall(x[1]);
1380
+ if (!cc) continue;
493
1381
  const close = matchPair(m, paren, "(", ")");
494
1382
  // `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);
1383
+ if (cc.name === "async" && /^\s*=>/.test(m.slice(close + 1, close + 8))) continue;
1384
+ // W1: `new OrderService().create(…)` / Python `OrderService().create(…)` —
1385
+ // the receiver is a constructor call, so the head is the class. Without it
1386
+ // the call is a bare `create`, and a bare name is never a class method.
1387
+ if (!cc.name.includes(".") && !cc.self) {
1388
+ const chain = m.slice(Math.max(0, x.index - 160), x.index);
1389
+ const nw = c.lang === "py" ? /\b([A-Z][\w]*)[ \t]*\([^()]*\)[ \t]*\.[ \t]*$/.exec(chain) : /\bnew[ \t]+([A-Za-z_$][\w$]*)[ \t]*\([^()]*\)[ \t]*\.[ \t]*$/.exec(chain);
1390
+ if (nw) cc.name = `${nw[1]}.${cc.name}`;
1391
+ else if (/\bsuper[ \t]*\([ \t]*\)[ \t]*\.[ \t]*$/.test(chain)) {
1392
+ // Python `super().m()` — a self call one level up (W2, G3).
1393
+ cc.self = true;
1394
+ cc.sup = true;
1395
+ }
1396
+ }
1397
+ const rec = { name: cc.name, line: lineOf(starts, paren), ...(cc.self ? { self: true } : {}), ...(cc.sup ? { sup: true } : {}), _p: paren };
1398
+ if (regParens && regParens.has(paren)) rec.reg = true;
1399
+ if (ARGS_WORTH.test(cc.name)) {
1400
+ rec.args = argPieces(c, paren, close);
1401
+ const assign = assignBefore(m, x.index);
1402
+ if (assign) rec.assign = assign;
1403
+ }
1404
+ out.push(rec);
1405
+ refsIn(m, starts, paren, close, out, c);
1406
+ }
1407
+ // W5 (G6). Ruby calls a method with NO parentheses — `@store.list`,
1408
+ // `handler.index`, `OrderStore.new` — so without this rung a Ruby file looks
1409
+ // as though it calls almost nothing. Only a RECEIVER form is taken: a bare
1410
+ // word with no parentheses is far more often a local variable, and guessing
1411
+ // there would invent edges, which is the one thing this engine must not do.
1412
+ if (c.lang === "rb") {
1413
+ const RBDOT = /@{0,2}[A-Za-z_]\w*(?:[ \t]*\.[ \t]*[A-Za-z_]\w*[?!]?)+(?![\w?!]|[ \t]*[(.])/g;
1414
+ while ((x = RBDOT.exec(m))) {
1415
+ const before = m.slice(Math.max(0, x.index - 6), x.index);
1416
+ if (/def[ ]*$/.test(before)) continue;
1417
+ const cc = normCall(x[0].replace(/^@+/, ""));
1418
+ if (!cc || !cc.name.includes(".")) continue;
1419
+ out.push({ name: cc.name, line: lineOf(starts, x.index), ...(cc.self ? { self: true } : {}), ...(cc.sup ? { sup: true } : {}), _p: x.index });
1420
+ }
498
1421
  }
499
1422
  return out;
500
1423
  }
@@ -507,8 +1430,9 @@ function scanCalls(m, starts, defParens) {
507
1430
  // it, so a local variable passed along never becomes an edge.
508
1431
  const REF_WORDS = new Set(["true", "false", "null", "undefined", "nil", "None", "True", "False", "NaN", "Infinity"]);
509
1432
  const REF_ARG = /^\s*(?:\.\.\.|&)?((?:[A-Za-z_$][\w$]*[ \t]*(?:\?\.|\.|->|::)[ \t]*)*[A-Za-z_$][\w$]*)\s*$/;
1433
+ const LARAVEL_REF = /^\s*\[\s*([A-Za-z_\\][\w\\]*)\s*::\s*class\s*,\s*(["'])\s*\2\s*\]\s*$/;
510
1434
 
511
- function refsIn(m, starts, open, close, out) {
1435
+ function refsIn(m, starts, open, close, out, c) {
512
1436
  let depth = 0;
513
1437
  let a = open + 1;
514
1438
  for (let k = open + 1; k <= close && k < m.length; k++) {
@@ -520,13 +1444,180 @@ function refsIn(m, starts, open, close, out) {
520
1444
  const piece = m.slice(a, k);
521
1445
  const t = REF_ARG.exec(piece);
522
1446
  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 } : {}) });
1447
+ const cc = normCall(t[1]);
1448
+ if (cc && !cc.sup) out.push({ name: cc.name, line: lineOf(starts, a + piece.indexOf(t[1])), ref: true, ...(cc.self ? { self: true } : {}) });
1449
+ } else if (c) {
1450
+ const lr = LARAVEL_REF.exec(piece);
1451
+ if (lr) {
1452
+ const method = firstStringIn(c.src, c.strAt, a, k);
1453
+ if (method) out.push({ name: `${lr[1].split("\\").pop()}.${method}`, line: lineOf(starts, a), ref: true });
1454
+ }
525
1455
  }
526
1456
  a = k + 1;
527
1457
  }
528
1458
  }
529
1459
 
1460
+ // ── instance aliases (v1.8.2 W2, G2) ────────────────────────────────────────
1461
+ // `const store = new OrderStore()` makes `store.list()` a call to
1462
+ // `OrderStore.list`. nestjs/nest measured 118 `maybe` callers on ONE symbol
1463
+ // from this shape alone. Each declaration below yields { line, local, cls,
1464
+ // self }: `self` is a class field (`this.x = new S()`, a constructor
1465
+ // param-property, a typed field), scoped to the class; anything else is scoped
1466
+ // to the symbol that contains the line (a function, a method, or the module).
1467
+ // A local re-bound to something that is not a class (`cls: null`) SHADOWS an
1468
+ // outer alias for that scope.
1469
+ const UP = "[A-Z][A-Za-z0-9_$]*";
1470
+ const ID = "[A-Za-z_$][A-Za-z0-9_$]*";
1471
+
1472
+ function aliasesOf(c, lang) {
1473
+ const { m, starts } = c;
1474
+ const out = [];
1475
+ const push = (pos, local, cls, self) => {
1476
+ if (!local || (cls && !/^[A-Z]/.test(cls))) return;
1477
+ out.push({ line: lineOf(starts, pos), local: local.replace(/^[$@]+/, ""), cls: cls || null, ...(self ? { self: true } : {}) });
1478
+ };
1479
+ let x;
1480
+ if (lang === "js" || lang === "ts") {
1481
+ // const x = new S() · const x: S = … · let x: S · const x = S() (a factory)
1482
+ const DECL = new RegExp(`\\b(?:const|let|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=;\\n]*)?=\\s*(?:new\\s+(${UP})|(${UP})\\s*\\(|([^\\n;]*))`, "g");
1483
+ while ((x = DECL.exec(m))) push(x.index, x[1], x[3] || x[4] || x[2] || null, false);
1484
+ const TYPED = new RegExp(`\\b(?:let|var)\\s+(${ID})\\s*:\\s*(${UP})\\s*[;\\n]`, "g");
1485
+ while ((x = TYPED.exec(m))) push(x.index, x[1], x[2], false);
1486
+ // this.x = new S() · constructor(private readonly x: S) · x: S; · x = new S();
1487
+ const THIS = new RegExp(`\\bthis\\.(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1488
+ while ((x = THIS.exec(m))) push(x.index, x[1], x[2], true);
1489
+ const PARAM = new RegExp(`\\b(?:public|private|protected|readonly)\\s+(?:readonly\\s+)?(${ID})\\s*[?!]?\\s*:\\s*(${UP})`, "g");
1490
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], true);
1491
+ const FIELD = new RegExp(`^[ \\t]+(?:(?:public|private|protected|static|readonly|declare|override|accessor)\\s+)*(${ID})\\s*[?!]?\\s*(?::\\s*(${UP})\\s*(?:=[^\\n;]*)?|=\\s*new\\s+(${UP}))\\s*(?:\\(|;)`, "gm");
1492
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[2] || x[3], true);
1493
+ } else if (lang === "py") {
1494
+ // x = S() · x: S = … · x: S · self.x = S() · def f(x: S)
1495
+ const ASSIGN = new RegExp(`^[ \\t]*(${ID})\\s*(?::\\s*(${UP})\\s*)?=\\s*(?:(${UP})\\s*\\(|([^\\n]*))`, "gm");
1496
+ while ((x = ASSIGN.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1497
+ const SELF = new RegExp(`\\bself\\.(${ID})\\s*(?::\\s*(${UP})\\s*)?=\\s*(?:(${UP})\\s*\\(|[^\\n]*)`, "g");
1498
+ while ((x = SELF.exec(m))) if (x[3] || x[2]) push(x.index, x[1], x[3] || x[2], true);
1499
+ const ANN = new RegExp(`[(,]\\s*(${ID})\\s*:\\s*(${UP})\\b`, "g");
1500
+ while ((x = ANN.exec(m))) push(x.index, x[1], x[2], false);
1501
+ } else if (lang === "go") {
1502
+ // x := S{} · x := &S{} · var x *S · x := NewS() · func (s *S) · f(s *S)
1503
+ const LIT = new RegExp(`\\b(${ID})\\s*:=\\s*&?(${UP})\\s*\\{`, "g");
1504
+ while ((x = LIT.exec(m))) push(x.index, x[1], x[2], false);
1505
+ const VAR = new RegExp(`\\bvar\\s+(${ID})\\s+\\*?(${UP})\\b`, "g");
1506
+ while ((x = VAR.exec(m))) push(x.index, x[1], x[2], false);
1507
+ const NEW = new RegExp(`\\b(${ID})\\s*:?=\\s*(?:[a-z][\\w]*\\.)?New(${UP})\\s*\\(`, "g");
1508
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2], false);
1509
+ const RECV = new RegExp(`^func\\s*\\(\\s*(${ID})\\s+\\*?(${UP})\\s*\\)`, "gm");
1510
+ while ((x = RECV.exec(m))) push(x.index, x[1], x[2], false);
1511
+ const PARAM = new RegExp(`[(,]\\s*(${ID})\\s+\\*?(${UP})\\s*[,)]`, "g");
1512
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], false);
1513
+ } else if (lang === "java" || lang === "cs") {
1514
+ // S x = … · var x = new S() · S x; (fields, locals, parameters)
1515
+ const TYPED = new RegExp(`\\b(${UP})(?:<[^>\\n]*>)?\\s+(${ID})\\s*(?:=|;|,|\\))`, "g");
1516
+ while ((x = TYPED.exec(m))) if (!KW.has(x[1]) && !/^(String|Integer|Long|Boolean|Double|Float|Object|List|Map|Set|Task|Void)$/.test(x[1])) push(x.index, x[2], x[1], false);
1517
+ const VAR = new RegExp(`\\bvar\\s+(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1518
+ while ((x = VAR.exec(m))) push(x.index, x[1], x[2], false);
1519
+ } else if (lang === "php") {
1520
+ // $x = new S() · private S $x · $this->x = new S()
1521
+ const NEW = new RegExp(`\\$(${ID})\\s*=\\s*new\\s+\\\\?(${UP})`, "g");
1522
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2].split("\\").pop(), false);
1523
+ const PROP = new RegExp(`\\b(?:public|private|protected)\\s+(?:readonly\\s+)?\\??\\\\?(${UP})\\s+\\$(${ID})`, "g");
1524
+ while ((x = PROP.exec(m))) push(x.index, x[2], x[1], true);
1525
+ const THIS = new RegExp(`\\$this->(${ID})\\s*=\\s*new\\s+\\\\?(${UP})`, "g");
1526
+ while ((x = THIS.exec(m))) push(x.index, x[1], x[2], true);
1527
+ } else if (lang === "rb") {
1528
+ // `store = OrderStore.new` · `@store = OrderStore.new` (an instance
1529
+ // variable IS the field form here, so it is a `self` alias).
1530
+ const LOCAL = new RegExp(`^[ \\t]*(${ID})\\s*=\\s*(${UP})(?:::\\w+)*\\.new\\b`, "gm");
1531
+ while ((x = LOCAL.exec(m))) push(x.index, x[1], x[2], false);
1532
+ const IVAR = new RegExp(`@@?(${ID})\\s*=\\s*(${UP})(?:::\\w+)*\\.new\\b`, "g");
1533
+ while ((x = IVAR.exec(m))) push(x.index, x[1], x[2], true);
1534
+ } else if (lang === "rs") {
1535
+ // `let s = Service::new()` · `let s: Service = …` · `s: Service` (a struct
1536
+ // field, so a `self.s` call) · `fn f(s: &Service)`.
1537
+ const LET = new RegExp(`\\blet\\s+(?:mut\\s+)?(${ID})\\s*(?::\\s*&?(?:mut\\s+)?(${UP}))?\\s*=\\s*(?:&?(${UP})(?:::\\w+)*\\s*(?:\\{|\\()|[^\\n;]*)`, "g");
1538
+ while ((x = LET.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1539
+ const FIELD = new RegExp(`^[ \\t]+(?:pub(?:\\([^)]*\\))?\\s+)?(${ID})\\s*:\\s*&?(?:mut\\s+)?(?:Arc<|Box<|Rc<|RwLock<|Mutex<)*(${UP})`, "gm");
1540
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[2], true);
1541
+ const PARAM = new RegExp(`[(,]\\s*(${ID})\\s*:\\s*&?(?:mut\\s+)?(?:Arc<|Box<|Rc<|State<)*(${UP})`, "g");
1542
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], false);
1543
+ } else if (lang === "kt") {
1544
+ // `val s = Service()` · `val s: Service` · `class X(private val s: Service)`
1545
+ const DECL = new RegExp(`\\b(?:val|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=\\n]*)?\\s*=\\s*(${UP})\\s*\\(`, "g");
1546
+ while ((x = DECL.exec(m))) push(x.index, x[1], x[3] || x[2] || null, false);
1547
+ const TYPED = new RegExp(`\\b(?:val|var)\\s+(${ID})\\s*:\\s*(${UP})`, "g");
1548
+ while ((x = TYPED.exec(m))) push(x.index, x[1], x[2], false);
1549
+ const PARAM = new RegExp(`[(,]\\s*(?:(?:private|protected|internal|public|val|var)\\s+)*(${ID})\\s*:\\s*(${UP})`, "g");
1550
+ while ((x = PARAM.exec(m))) push(x.index, x[1], x[2], true);
1551
+ // A `val` at class level is a FIELD: it is reached from a method with no
1552
+ // receiver, so it has to be recorded as a `self` alias, not a local.
1553
+ const FIELD = new RegExp(`^[ \\t]+(?:(?:private|protected|internal|public|open|override|lateinit|const)\\s+)*(?:val|var)\\s+(${ID})\\s*(?::\\s*(${UP})[^=\\n]*)?\\s*=\\s*(${UP})\\s*\\(`, "gm");
1554
+ while ((x = FIELD.exec(m))) push(x.index, x[1], x[3] || x[2], true);
1555
+ } else if (lang === "c") {
1556
+ // `Service s;` · `Service* s = new Service();` · a member declared in the
1557
+ // class body is reached without `this->`, like Java and C#.
1558
+ const NEW = new RegExp(`\\b(${ID})\\s*=\\s*new\\s+(${UP})`, "g");
1559
+ while ((x = NEW.exec(m))) push(x.index, x[1], x[2], false);
1560
+ const TYPED = new RegExp(`\\b(${UP})(?:<[^>\\n]*>)?\\s*[*&]?\\s+(${ID})\\s*(?:=|;|,|\\))`, "g");
1561
+ while ((x = TYPED.exec(m))) if (!isKw(lang, x[1])) push(x.index, x[2], x[1], false);
1562
+ const FIELD = new RegExp(`^[ \\t]+(${UP})(?:<[^>\\n]*>)?\\s*[*&]?\\s+(${ID})\\s*;`, "gm");
1563
+ while ((x = FIELD.exec(m))) if (!isKw(lang, x[1])) push(x.index, x[2], x[1], true);
1564
+ }
1565
+ return out;
1566
+ }
1567
+
1568
+ // ── re-exports (v1.8.2 W2, G4) ──────────────────────────────────────────────
1569
+ // A barrel defines nothing and re-exports: `export * from "./x"`,
1570
+ // `export { a as b } from "./x"`, `module.exports = require("./x")`,
1571
+ // `exports.a = require("./x").a`, and every import in a Python `__init__.py`.
1572
+ // The IMPORT rung on read follows these to the defining file.
1573
+ function reexportsFor(lang, src, m, rel, imports) {
1574
+ const out = [];
1575
+ let x;
1576
+ if (lang === "js" || lang === "ts") {
1577
+ const STAR = /^[ \t]*export[ \t]+\*[ \t]+from[ \t]*['"]([^'"\n]+)['"]/gm;
1578
+ while ((x = STAR.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[1], names: "*" });
1579
+ const NAMED = /^[ \t]*export[ \t]+(?:type[ \t]+)?\{([^}]*)\}[ \t]*from[ \t]*['"]([^'"\n]+)['"]/gm;
1580
+ while ((x = NAMED.exec(src))) {
1581
+ if (!codeAt(src, m, x.index)) continue;
1582
+ const names = [];
1583
+ for (const p of x[1].split(",")) {
1584
+ const mm = /^\s*(?:type\s+)?([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(p);
1585
+ if (mm) names.push({ local: mm[2] || mm[1], orig: mm[1] });
1586
+ }
1587
+ if (names.length) out.push({ at: x.index, from: x[2], names });
1588
+ }
1589
+ const WHOLE = /module\.exports[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)[ \t]*;?[ \t]*$/gm;
1590
+ while ((x = WHOLE.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[1], names: "*" });
1591
+ const PROP = /(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=[ \t]*require\([ \t]*['"]([^'"\n]+)['"][ \t]*\)\.([A-Za-z_$][\w$]*)/g;
1592
+ while ((x = PROP.exec(src))) if (codeAt(src, m, x.index)) out.push({ at: x.index, from: x[2], names: [{ local: x[1], orig: x[3] }] });
1593
+ // `export { a, b }` of names that were IMPORTED (not defined here) is a
1594
+ // re-export too: `import { A } from "./a"; export { A };`.
1595
+ const LOCAL = /^[ \t]*export[ \t]*\{([^}]*)\}[ \t]*;?[ \t]*$/gm;
1596
+ while ((x = LOCAL.exec(src))) {
1597
+ if (!codeAt(src, m, x.index)) continue;
1598
+ for (const p of x[1].split(",")) {
1599
+ const mm = /^\s*([\w$]+)(?:\s+as\s+([\w$]+))?\s*$/.exec(p);
1600
+ if (!mm) continue;
1601
+ for (const imp of imports || [])
1602
+ for (const b of imp.bindings || []) if (b.local === mm[1] && b.kind === "named") out.push({ at: x.index, from: imp.from, names: [{ local: mm[2] || mm[1], orig: b.orig }] });
1603
+ }
1604
+ }
1605
+ // Source order, whatever shape found them.
1606
+ out.sort((a, b) => a.at - b.at);
1607
+ for (const r of out) delete r.at;
1608
+ } else if (lang === "py" && /(^|\/)__init__\.py$/.test(rel)) {
1609
+ for (const imp of imports || []) {
1610
+ if (!imp.from) continue;
1611
+ if (!(imp.bindings || []).length) continue;
1612
+ out.push({ from: imp.from, names: imp.bindings.map((b) => ({ local: b.local, orig: b.orig })) });
1613
+ }
1614
+ if (/^[ \t]*from[ \t]+[.\w]+[ \t]+import[ \t]+\*/m.test(src)) {
1615
+ for (const mm of src.matchAll(/^[ \t]*from[ \t]+([.\w]+)[ \t]+import[ \t]+\*/gm)) if (codeAt(src, m, mm.index)) out.push({ from: mm[1], names: "*" });
1616
+ }
1617
+ }
1618
+ return out.slice(0, 200);
1619
+ }
1620
+
530
1621
  // ── imports ─────────────────────────────────────────────────────────────────
531
1622
  // A statement is read from the ORIGINAL text (its path is a string), but only
532
1623
  // where the mask agrees the keyword is code — never inside a comment.
@@ -634,6 +1725,47 @@ function importsFor(lang, src, m, starts) {
634
1725
  }
635
1726
  const INC = /\b(?:require|include)(?:_once)?[ \t]*\(?[ \t]*['"]([^'"\n]+\.php)['"]/g;
636
1727
  while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
1728
+ } else if (lang === "rb") {
1729
+ // `require_relative "../store"` is a path; `require "store"` is a load
1730
+ // path entry, matched by suffix. Neither names the constants it brings in,
1731
+ // so the file is the binding and the UNIQUE rung names the symbol.
1732
+ const REQ = /^[ \t]*require(_relative)?[ \t]*\(?[ \t]*['"]([^'"\n]+)['"]/gm;
1733
+ while ((x = REQ.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[2], line: line(x.index), bindings: [], ...(x[1] ? { include: true } : {}) });
1734
+ } else if (lang === "rs") {
1735
+ // `mod store;` names a sibling file; `use crate::store::OrderStore` names
1736
+ // a symbol in one. A `{a, b}` group brings in several names at once.
1737
+ const MOD = /^[ \t]*(?:pub[ \t]+)?mod[ \t]+([A-Za-z_]\w*)[ \t]*;/gm;
1738
+ while ((x = MOD.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], mod: true });
1739
+ const USE = /^[ \t]*(?:pub[ \t]+)?use[ \t]+([^;\n]+);/gm;
1740
+ while ((x = USE.exec(src))) {
1741
+ if (!codeAt(src, m, x.index)) continue;
1742
+ const raw = x[1].replace(/\s+/g, "");
1743
+ const g = /^(.*?)::\{(.+)\}$/.exec(raw);
1744
+ const bindings = [];
1745
+ const take = (p) => {
1746
+ const mm = /^(?:.*::)?(\w+)(?:as(\w+))?$/.exec(p);
1747
+ if (mm && mm[1] !== "self" && mm[1] !== "*") bindings.push({ local: mm[2] || mm[1], orig: mm[1], kind: "named" });
1748
+ };
1749
+ if (g) {
1750
+ for (const p of g[2].split(",")) if (p) take(p);
1751
+ out.push({ from: g[1], line: line(x.index), bindings });
1752
+ } else {
1753
+ take(raw);
1754
+ out.push({ from: raw.replace(/::\w+$/, ""), line: line(x.index), bindings });
1755
+ }
1756
+ }
1757
+ } else if (lang === "kt") {
1758
+ const IMP = /^[ \t]*import[ \t]+([\w.]+?)(\.\*)?(?:[ \t]+as[ \t]+(\w+))?[ \t]*$/gm;
1759
+ while ((x = IMP.exec(src))) {
1760
+ if (!codeAt(src, m, x.index)) continue;
1761
+ const last = x[1].split(".").pop();
1762
+ if (x[2]) out.push({ from: x[1], line: line(x.index), bindings: [], star: true });
1763
+ else out.push({ from: x[1], line: line(x.index), bindings: [{ local: x[3] || last, orig: last, kind: "named" }] });
1764
+ }
1765
+ } else if (lang === "c") {
1766
+ // Only a QUOTED include is a file in this repository; `<vector>` is not.
1767
+ const INC = /^[ \t]*#[ \t]*include[ \t]*"([^"\n]+)"/gm;
1768
+ while ((x = INC.exec(src))) if (codeAt(src, m, x.index)) out.push({ from: x[1], line: line(x.index), bindings: [], include: true });
637
1769
  }
638
1770
  return out;
639
1771
  }
@@ -653,11 +1785,11 @@ const FS_RE = {
653
1785
  FS_RE.ts = FS_RE.js;
654
1786
  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
1787
 
656
- function stringAt(src, strings, pos) {
1788
+ function stringAt(src, strAt, pos) {
657
1789
  let k = pos;
658
1790
  while (k < src.length && /\s/.test(src[k])) k++;
659
1791
  if (!/['"`]/.test(src[k] || "")) return null;
660
- const s = strings.find((t) => t.start === k + 1);
1792
+ const s = strAt.get(k + 1);
661
1793
  return s ? src.slice(s.start, s.end) : null;
662
1794
  }
663
1795
 
@@ -672,7 +1804,7 @@ function parenFor(src, starts, call) {
672
1804
  return p >= 0 && p < le ? p : null;
673
1805
  }
674
1806
 
675
- function effectsOf(lang, src, m, starts, strings, calls) {
1807
+ function effectsOf(lang, src, m, starts, strings, strAt, calls) {
676
1808
  const out = [];
677
1809
  for (const s of strings) {
678
1810
  const text = src.slice(s.start, s.end);
@@ -686,41 +1818,312 @@ function effectsOf(lang, src, m, starts, strings, calls) {
686
1818
  if (name) out.push({ type: "env", text: name, line: lineOf(starts, x.index) });
687
1819
  }
688
1820
  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 });
1821
+ for (const cc of calls) {
1822
+ if (HTTP_RE.test(cc.name)) {
1823
+ const p = parenFor(src, starts, cc);
1824
+ const url = p != null ? stringAt(src, strAt, p + 1) : null;
1825
+ out.push({ type: "http", text: url ? `${cc.name} ${clip(url, 60)}` : cc.name, line: cc.line });
1826
+ } else if (DB_RE.test(cc.name)) {
1827
+ out.push({ type: "db", text: cc.name, line: cc.line });
1828
+ } else if (fsRe && fsRe.test(cc.name)) {
1829
+ out.push({ type: "fs", text: cc.name, line: cc.line });
1830
+ } else if (lang === "py" && cc.name === "open") {
1831
+ const p = parenFor(src, starts, cc);
1832
+ if (p != null && /^[^)]*?,\s*(?:mode\s*=\s*)?['"][wax]/.test(src.slice(p + 1, p + 120))) out.push({ type: "fs", text: "open (write)", line: cc.line });
701
1833
  }
702
1834
  }
703
1835
  return out;
704
1836
  }
705
1837
 
1838
+ // ── URLs, mounts and prefixes (v1.8.2 W1) ───────────────────────────────────
1839
+ // A URL is a string that starts with `/`. A template or an f-string whose
1840
+ // leading expression is unknown (`${base}/p`, `f"{base}/p"`) and a `BASE + "/p"`
1841
+ // concatenation are SUFFIXES: the path is known, the prefix is not, and the
1842
+ // resolver matches them by their tail only.
1843
+ function urlText(s) {
1844
+ if (s == null) return null;
1845
+ let t = String(s).trim();
1846
+ let suffix = false;
1847
+ const lead = /^(\$\{[^}]*\}|\{[^}]*\})+/.exec(t);
1848
+ if (lead) {
1849
+ t = t.slice(lead[0].length);
1850
+ suffix = true;
1851
+ }
1852
+ const abs = /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(\/.*)?$/i.exec(t);
1853
+ if (abs) t = abs[2] || "/";
1854
+ else if (/^[a-z]+:\/\//i.test(t)) return null; // another host — not a route here
1855
+ if (!t.startsWith("/") || /[\r\n]/.test(t) || t.length > 200) return null;
1856
+ return { url: t, suffix };
1857
+ }
1858
+
1859
+ const GO_METHOD = /^http\.Method(Get|Post|Put|Patch|Delete|Head|Options)$/;
1860
+
1861
+ // The URL an HTTP-shaped call names, with its method — or null.
1862
+ function urlOf(lang, call) {
1863
+ const args = call.args;
1864
+ if (!args || !args.length) return null;
1865
+ const parts = call.name.split(".");
1866
+ const last = parts[parts.length - 1];
1867
+ const head = parts.length > 1 ? parts[parts.length - 2] : null;
1868
+ const pos = args.filter((a) => !a.kw);
1869
+ const kw = {};
1870
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1871
+ // `BASE + "/p"` is one piece whose tail is known: it becomes `{}/p`, which
1872
+ // `urlText` reads as a SUFFIX.
1873
+ const strArg = (i) => (pos[i] && pos[i].s != null ? pos[i].s : pos[i] && pos[i].cat != null ? "{}" + pos[i].cat : null);
1874
+ let method = null;
1875
+ let raw = null;
1876
+ if (last === "open" && !(kw.method && kw.method.s)) return null;
1877
+ if (HTTP_VERBS.has(last) && head !== "Route" && !/^(cache|map|store|redis|client\.hgetall|localStorage|params|headers|searchParams|Map|WeakMap|process|obj|opts|options|config|env|form|url)$/i.test(head || "")) {
1878
+ method = VERB_OF(last);
1879
+ raw = strArg(0) != null ? strArg(0) : kw.url && kw.url.s != null ? kw.url.s : null;
1880
+ // `$this->json('POST', '/p')`, `client.open("/p", method="post")`
1881
+ } else if (last === "json" && lang === "php" && strArg(0) && /^[A-Z]+$/.test(strArg(0))) {
1882
+ method = strArg(0);
1883
+ raw = strArg(1);
1884
+ } else if (last === "fetch" || last === "axios" || last === "request" || last === "open" || last === "perform") {
1885
+ raw = strArg(0) != null ? strArg(0) : kw.url && kw.url.s != null ? kw.url.s : null;
1886
+ const o = (pos[1] && pos[1].obj) || (pos[0] && pos[0].obj) || null;
1887
+ method = (o && o.method && o.method.toUpperCase()) || (kw.method && kw.method.s && kw.method.s.toUpperCase()) || "ANY";
1888
+ if (last === "perform" && !raw) return null;
1889
+ } else if (/^(NewRequest|NewRequestWithContext)$/.test(last) && (head === "http" || head === "httptest")) {
1890
+ const i = last === "NewRequestWithContext" ? 1 : 0;
1891
+ const mm = strArg(i);
1892
+ method = mm ? mm.toUpperCase() : pos[i] && pos[i].id && GO_METHOD.test(pos[i].id) ? GO_METHOD.exec(pos[i].id)[1].toUpperCase() : "ANY";
1893
+ raw = strArg(i + 1);
1894
+ } else if (/^(Get|Post|Head)$/.test(last) && head === "http") {
1895
+ method = last.toUpperCase();
1896
+ raw = strArg(0);
1897
+ } else if (/^(GetAsync|PostAsync|PutAsync|DeleteAsync|PatchAsync)$/.test(last)) {
1898
+ method = last.replace(/Async$/, "").toUpperCase();
1899
+ raw = strArg(0);
1900
+ } else return null;
1901
+ const u = urlText(raw);
1902
+ if (!u) return null;
1903
+ return { method: method || "ANY", url: u.url, ...(u.suffix ? { suffix: true } : {}), line: call.line };
1904
+ }
1905
+
1906
+ // `app.use("/orders", ordersRouter)` and its cousins → { prefix, target }.
1907
+ // `target` is a NAME (an import binding, a dotted name), an inline require's
1908
+ // specifier, or a Django `include()` module — resolved on read.
1909
+ function mountOf(lang, call) {
1910
+ const args = call.args;
1911
+ if (!args || !args.length) return null;
1912
+ const last = call.name.split(".").pop();
1913
+ const pos = args.filter((a) => !a.kw);
1914
+ const kw = {};
1915
+ for (const a of args) if (a.kw) kw[a.kw] = a;
1916
+ const tgt = (a) => (a ? (a.req != null ? a.req : a.inc != null ? a.inc : a.id != null ? a.id : null) : null);
1917
+ const norm = (p) => (p == null ? null : String(p).startsWith("/") ? String(p) : "/" + String(p));
1918
+ if ((lang === "js" || lang === "ts") && last === "use" && pos[0] && pos[0].s != null && pos[0].s.startsWith("/")) {
1919
+ const t = pos.slice(1).map(tgt).find(Boolean);
1920
+ return t ? { prefix: pos[0].s, target: t } : null;
1921
+ }
1922
+ if ((lang === "js" || lang === "ts") && last === "register" && pos[0] && tgt(pos[0])) {
1923
+ const o = pos[1] && pos[1].obj;
1924
+ return { prefix: (o && o.prefix) || "", target: tgt(pos[0]) };
1925
+ }
1926
+ if (lang === "py" && (last === "include_router" || last === "register_blueprint" || last === "mount")) {
1927
+ if (last === "mount") return pos[0] && pos[0].s != null && tgt(pos[1]) ? { prefix: pos[0].s, target: tgt(pos[1]) } : null;
1928
+ const t = tgt(pos[0]);
1929
+ if (!t) return null;
1930
+ const p = (kw.prefix && kw.prefix.s) || (kw.url_prefix && kw.url_prefix.s) || (pos[1] && pos[1].s) || "";
1931
+ return { prefix: p, target: t };
1932
+ }
1933
+ if (lang === "py" && /^(path|re_path|url)$/.test(last) && pos[0] && pos[0].s != null && pos[1] && pos[1].inc != null) {
1934
+ return { prefix: norm(pos[0].s.replace(/^\^/, "")), target: pos[1].inc, module: true };
1935
+ }
1936
+ if (lang === "go" && (last === "Mount" || last === "Group") && pos[0] && pos[0].s != null && pos[1] && tgt(pos[1])) {
1937
+ return { prefix: pos[0].s, target: tgt(pos[1]) };
1938
+ }
1939
+ return null;
1940
+ }
1941
+
1942
+ // `router = APIRouter(prefix="/orders")` / `bp = Blueprint("x", __name__,
1943
+ // url_prefix="/x")` / `new Router({ prefix: "/x" })` — the prefix a VARIABLE
1944
+ // carries in this file. Decorator routes on it and `router.get` registrations
1945
+ // on it fold it into their name.
1946
+ function prefixOf(call) {
1947
+ const args = call.args;
1948
+ if (!args || !call.assign) return null;
1949
+ const last = call.name.split(".").pop();
1950
+ if (!/^(APIRouter|Blueprint|Router)$/.test(last)) return null;
1951
+ for (const a of args) {
1952
+ if (a.kw === "prefix" || a.kw === "url_prefix") return a.s != null ? a.s : null;
1953
+ if (a.obj && a.obj.prefix) return a.obj.prefix;
1954
+ }
1955
+ return "";
1956
+ }
1957
+
1958
+ const joinPath = (prefix, p) => {
1959
+ const a = String(prefix || "");
1960
+ let b = String(p || "");
1961
+ if (b && !b.startsWith("/")) b = "/" + b;
1962
+ const out = (a + b).replace(/\/{2,}/g, "/");
1963
+ return out || "/";
1964
+ };
1965
+
706
1966
  // ── assembly ────────────────────────────────────────────────────────────────
707
- function finalize(rel, lang, src, m, starts, strings, defs, calls, imports, exportsSet, extractor) {
1967
+ function finalize(c, lang, defs, calls, imports, exportsSet, extractor) {
1968
+ const { src, m, starts, strings, strAt } = c;
1969
+ const rel = c.rel;
708
1970
  const nLines = starts.length;
1971
+
1972
+ // W1: the prefix each router VARIABLE carries in this file, and the prefix of
1973
+ // each class (a decorator on the class).
1974
+ const varPrefix = new Map();
1975
+ for (const cc of calls) {
1976
+ if (cc.ref) continue;
1977
+ const p = prefixOf(cc);
1978
+ if (p != null) varPrefix.set(cc.assign, p);
1979
+ }
1980
+ const classPrefix = new Map();
1981
+ for (const d of defs) {
1982
+ if (d.kind !== "class" || !d.decos) continue;
1983
+ for (const deco of d.decos) {
1984
+ const r = decoRoute(deco, true);
1985
+ if (r && r.prefix != null) classPrefix.set(d.name, r.prefix);
1986
+ }
1987
+ }
1988
+ // Route names. A registration (`router.get("/p", …)`) carries its head's
1989
+ // prefix; a decorated handler carries its class's, or its decorator head's.
1990
+ const aliases = [];
1991
+ for (const d of defs) {
1992
+ if (d.kind === "route") {
1993
+ const pre = varPrefix.get(d.head) || "";
1994
+ d.name = `${d.method} ${clip(joinPath(pre, d.path), 60)}`;
1995
+ d.qname = d.name;
1996
+ continue;
1997
+ }
1998
+ if (!d.decos) continue;
1999
+ const routes = [];
2000
+ for (const deco of d.decos) {
2001
+ const r = decoRoute(deco, false);
2002
+ if (!r) continue;
2003
+ const head = String(deco.name).split(".").slice(0, -1).join(".");
2004
+ const cls = d.qname.includes(".") ? d.qname.split(".").slice(0, -1).pop() : null;
2005
+ let pre = (cls && classPrefix.get(cls)) || varPrefix.get(head) || "";
2006
+ // ASP.NET: `[Route("api/[controller]")]` names the class without its suffix.
2007
+ if (cls && /\[controller\]/i.test(pre)) pre = pre.replace(/\[controller\]/gi, cls.replace(/Controller$/, "").toLowerCase());
2008
+ for (const rt of r.routes) routes.push({ method: rt.method, path: joinPath(pre, rt.path) });
2009
+ }
2010
+ if (routes.length) {
2011
+ d.route = `${routes[0].method} ${clip(routes[0].path, 60)}`;
2012
+ for (const rt of routes) aliases.push({ def: d, name: `${rt.method} ${clip(rt.path, 60)}` });
2013
+ }
2014
+ }
2015
+
709
2016
  const owner = new Int32Array(nLines + 2).fill(-1);
710
2017
  const order = defs.map((_, i) => i).sort((a, b) => defs[b].e - defs[b].s - (defs[a].e - defs[a].s));
711
2018
  for (const i of order) for (let L = defs[i].s; L <= defs[i].e && L <= nLines; L++) owner[L] = i;
712
2019
 
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
- }));
2020
+ // W2 (G2): instance aliases, scoped. `classAt(line)` is the innermost class
2021
+ // holding a line; `ownerOf(line)` the innermost symbol (−1 = the module).
2022
+ const classDefs = defs.map((d, i) => ({ d, i })).filter((x) => x.d.kind === "class");
2023
+ const classAt = (line) => {
2024
+ let best = null;
2025
+ for (const x of classDefs) if (x.d.s <= line && line <= x.d.e && (!best || x.d.e - x.d.s < best.d.e - best.d.s)) best = x;
2026
+ return best ? best.d : null;
2027
+ };
2028
+ const ownerOf = (line) => (line >= 0 && line <= nLines ? owner[line] : -1);
2029
+ const localAl = new Map(); // owner index → Map<local, cls|null>
2030
+ const classAl = new Map(); // class name → Map<field, cls>
2031
+ for (let al of aliasesOf(c, lang)) {
2032
+ // W5 (G6): a `self` alias with no enclosing class is not a field — it is
2033
+ // an indented local (a Kotlin `val` inside a top-level function). Reading
2034
+ // it as a local is what it is; dropping it loses the edge.
2035
+ if (al.self && !classAt(al.line)) al = { ...al, self: false };
2036
+ if (al.self) {
2037
+ const cl = classAt(al.line);
2038
+ if (!cl) continue;
2039
+ const mm = classAl.get(cl.name) || new Map();
2040
+ mm.set(al.local, al.cls);
2041
+ classAl.set(cl.name, mm);
2042
+ } else {
2043
+ const idx = ownerOf(al.line);
2044
+ const mm = localAl.get(idx) || new Map();
2045
+ // The first declaration in a scope wins; a later re-binding of the same
2046
+ // name in the same scope is rare and a guess either way.
2047
+ if (!mm.has(al.local)) mm.set(al.local, al.cls);
2048
+ localAl.set(idx, mm);
2049
+ }
2050
+ }
2051
+ const classOfDef = (d) => (d && d.kind === "method" && d.qname.includes(".") ? d.qname.split(".").slice(0, -1).pop() : d && d.kind === "class" ? d.name : null);
2052
+ const aliasFor = (cc) => {
2053
+ if (cc.ref || cc.sup || !cc.name.includes(".")) return null;
2054
+ const parts = cc.name.split(".");
2055
+ if (parts.length !== 2) return null;
2056
+ const head = parts[0];
2057
+ const idx = ownerOf(cc.line);
2058
+ const d = idx >= 0 ? defs[idx] : null;
2059
+ if (cc.self) {
2060
+ const cn = classOfDef(d) || (classAt(cc.line) || {}).name;
2061
+ const mm = cn ? classAl.get(cn) : null;
2062
+ const cls = mm ? mm.get(head) : undefined;
2063
+ return cls ? { cls, head } : null;
2064
+ }
2065
+ const scoped = localAl.get(idx);
2066
+ if (scoped && scoped.has(head)) {
2067
+ const cls = scoped.get(head);
2068
+ return cls ? { cls, head } : null; // null = shadowed here
2069
+ }
2070
+ const modAl = localAl.get(-1);
2071
+ if (idx !== -1 && modAl && modAl.has(head)) {
2072
+ const cls = modAl.get(head);
2073
+ return cls ? { cls, head } : null;
2074
+ }
2075
+ // Java, C#, Kotlin, C++ and Ruby all reach a field with no `this.`
2076
+ // (`@store` in Ruby is the field itself, written without `self.`).
2077
+ if (lang === "java" || lang === "cs" || lang === "kt" || lang === "c" || lang === "rb") {
2078
+ const cn = classOfDef(d);
2079
+ const mm = cn ? classAl.get(cn) : null;
2080
+ const cls = mm ? mm.get(head) : undefined;
2081
+ return cls ? { cls, head } : null;
2082
+ }
2083
+ return null;
2084
+ };
2085
+ // W5 (G6). In Ruby and Kotlin a bare call inside a method is a call on the
2086
+ // object — `ok(body)` means `self.ok(body)` — and that is what lets it reach
2087
+ // the base class. It is marked self only when THIS FILE has no top-level
2088
+ // function of the same name, so a real module function still wins where the
2089
+ // language would give it the call.
2090
+ if (lang === "rb" || lang === "kt") {
2091
+ const topLevel = new Set(defs.filter((d) => d.kind === "function").map((d) => d.name));
2092
+ for (const imp of imports || []) for (const b of imp.bindings || []) topLevel.add(b.local);
2093
+ for (const cc of calls) {
2094
+ if (cc.ref || cc.self || cc.name.includes(".") || topLevel.has(cc.name)) continue;
2095
+ const d = defs[ownerOf(cc.line)];
2096
+ if (d && d.kind === "method") cc.self = true;
2097
+ }
2098
+ }
2099
+ for (const cc of calls) {
2100
+ const al = aliasFor(cc);
2101
+ if (!al) continue;
2102
+ cc.name = `${al.cls}.${cc.name.split(".")[1]}`;
2103
+ cc.alias = al.head;
2104
+ delete cc.self;
2105
+ }
2106
+
2107
+ // N1: the sentence the author already wrote, read out of the raw source (the
2108
+ // mask blanks comments, so it cannot come from `m`).
2109
+ const rawLines = src.split(/\r?\n/);
2110
+ const syms = defs.map((d) => {
2111
+ const doc = docFor(rawLines, lang, d);
2112
+ return {
2113
+ id: "",
2114
+ name: d.name,
2115
+ qname: d.qname || d.name,
2116
+ kind: d.kind,
2117
+ lines: [d.s, d.e],
2118
+ exported: !!(d.exported || (exportsSet && exportsSet.has(d.name))),
2119
+ body_hash: "",
2120
+ calls: [],
2121
+ effects: [],
2122
+ ...(doc ? { doc } : {}),
2123
+ ...(d.route ? { route: d.route } : {}),
2124
+ ...(d.bases && d.bases.length ? { bases: d.bases } : {}),
2125
+ };
2126
+ });
724
2127
  const mod = { id: "", name: "<module>", qname: "<module>", kind: "module", lines: [1, nLines], exported: false, body_hash: "", calls: [], effects: [] };
725
2128
  const at = (line) => (owner[line] >= 0 ? syms[owner[line]] : mod);
726
2129
 
@@ -735,15 +2138,35 @@ function finalize(rel, lang, src, m, starts, strings, defs, calls, imports, expo
735
2138
  const was = cutAt.get(t);
736
2139
  if (was === undefined || line < was) cutAt.set(t, line);
737
2140
  };
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);
2141
+ for (const cc of calls) {
2142
+ // A ref is kept when its head is defined here or imported. Go's package
2143
+ // scope is the DIRECTORY, so a bare Go identifier is kept too and the
2144
+ // package rung on read decides (`mux.HandleFunc("/p", listOrders)`).
2145
+ const refHead = cc.self ? cc.name.split(".").pop() : cc.name.split(".")[0];
2146
+ if (cc.ref && !known.has(refHead) && !(lang === "go" && !cc.name.includes("."))) continue;
2147
+ const t = at(cc.line);
741
2148
  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);
2149
+ t.calls.push({
2150
+ name: cc.name,
2151
+ line: cc.line,
2152
+ ...(cc.self ? { self: true } : {}),
2153
+ ...(cc.sup ? { sup: true } : {}),
2154
+ ...(cc.ref ? { ref: true } : {}),
2155
+ ...(cc.alias ? { alias: cc.alias } : {}),
2156
+ });
2157
+ else cut(t, cc.line);
2158
+ if (cc.ref || !cc.args) continue;
2159
+ const u = cc.reg ? null : urlOf(lang, cc);
2160
+ if (u) {
2161
+ if (!t.urls) t.urls = [];
2162
+ if (t.urls.length < MAX_URLS_PER_SYMBOL) t.urls.push(u);
2163
+ else cut(t, cc.line);
2164
+ }
2165
+ const mt = mountOf(lang, cc);
2166
+ if (mt) (t.mounts || (t.mounts = [])).push({ ...mt, line: cc.line });
744
2167
  }
745
2168
  // 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))) {
2169
+ for (const ef of effectsOf(lang, src, m, starts, strings, strAt, calls.filter((cc) => !cc.ref))) {
747
2170
  const t = at(ef.line);
748
2171
  if (t.effects.length >= MAX_EFFECTS_PER_SYMBOL) {
749
2172
  cut(t, ef.line);
@@ -767,45 +2190,162 @@ function finalize(rel, lang, src, m, starts, strings, defs, calls, imports, expo
767
2190
  }
768
2191
 
769
2192
  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));
2193
+ const assignId = (s) => {
774
2194
  let id = `${rel}#${s.qname}`;
775
2195
  const k = seen.get(id) || 0;
776
2196
  seen.set(id, k + 1);
777
2197
  if (k) id += `~${k + 1}`;
778
2198
  s.id = id;
2199
+ };
2200
+ for (const s of syms) {
2201
+ const a = starts[s.lines[0] - 1] || 0;
2202
+ const b = s.lines[1] < nLines ? starts[s.lines[1]] : src.length;
2203
+ s.body_hash = hash(src.slice(a, b));
2204
+ assignId(s);
779
2205
  }
780
- if (mod.calls.length || mod.effects.length) {
2206
+ // W1: the alias ROUTE symbol for a decorated handler. Same lines and the same
2207
+ // body hash as the handler; it CALLS the handler (one LOCAL edge on read), so
2208
+ // `impact` and `ctx` walk test → route → handler with no special case.
2209
+ for (const al of aliases) {
2210
+ const h = syms[defs.indexOf(al.def)];
2211
+ if (!h) continue;
2212
+ const s = {
2213
+ id: "",
2214
+ name: al.name,
2215
+ qname: al.name,
2216
+ kind: "route",
2217
+ lines: h.lines.slice(),
2218
+ exported: false,
2219
+ body_hash: h.body_hash,
2220
+ calls: [{ name: h.qname, line: h.lines[0], ...(h.kind === "method" ? {} : {}) }],
2221
+ effects: [],
2222
+ ...(h.doc ? { doc: h.doc } : {}),
2223
+ handler: h.id,
2224
+ };
2225
+ assignId(s);
2226
+ syms.push(s);
2227
+ }
2228
+ if (mod.calls.length || mod.effects.length || mod.urls || mod.mounts) {
781
2229
  mod.id = `${rel}#<module>`;
782
2230
  syms.push(mod);
783
2231
  }
784
- return { extractor, imports, symbols: syms, coverage: partial.length ? "partial" : "full", ...(partial.length ? { partial } : {}) };
2232
+ const reexports = reexportsFor(lang, src, m, rel, imports);
2233
+ return {
2234
+ extractor,
2235
+ imports,
2236
+ symbols: syms,
2237
+ // K1 (v1.8.2 W4b): the line count. The gain meter's counterfactual needs
2238
+ // the AVERAGE line length of this file to price "an 80-line range read",
2239
+ // and `bytes / lines` is that, for free, from the parse that just ran.
2240
+ lines: nLines,
2241
+ coverage: partial.length ? "partial" : "full",
2242
+ ...(partial.length ? { partial } : {}),
2243
+ ...(reexports.length ? { reexports } : {}),
2244
+ };
785
2245
  }
786
2246
 
787
- function extractOne(item, borrowed) {
2247
+ function context(item) {
788
2248
  const { rel, lang, src } = item;
789
- resetCoverage();
790
2249
  const { masked: m, strings } = mask(src, lang);
791
- const starts = lineIndex(src);
2250
+ const strAt = new Map();
2251
+ for (const s of strings) strAt.set(s.start, s);
2252
+ return { rel, lang, src, m, strings, strAt, starts: lineIndex(src) };
2253
+ }
2254
+
2255
+ // W5 (G6). A single-file component is a `<script>` block inside markup. Blank
2256
+ // everything outside it, keeping every newline, so the js/ts rung reads the
2257
+ // script and every line number is still the FILE's own line number.
2258
+ function sfcSource(src) {
2259
+ const out = src.split("");
2260
+ let lang = "js";
2261
+ const keep = [];
2262
+ const lower = src.toLowerCase();
2263
+ const RE = /<script\b([^>]*)>/gi;
2264
+ let x;
2265
+ while ((x = RE.exec(src))) {
2266
+ const open = x.index + x[0].length;
2267
+ const close = lower.indexOf("</script>", open);
2268
+ const end = close < 0 ? src.length : close;
2269
+ if (/lang[ \t]*=[ \t]*["']?(ts|typescript)/i.test(x[1])) lang = "ts";
2270
+ keep.push([open, end]);
2271
+ RE.lastIndex = end;
2272
+ }
2273
+ let at = 0;
2274
+ const blank = (a, b) => {
2275
+ for (let k = a; k < b; k++) if (out[k] !== "\n" && out[k] !== "\r") out[k] = " ";
2276
+ };
2277
+ for (const [a, b] of keep) {
2278
+ blank(at, a);
2279
+ at = b;
2280
+ }
2281
+ blank(at, src.length);
2282
+ return { src: out.join(""), lang };
2283
+ }
2284
+
2285
+ function extractOne(item, borrowed) {
2286
+ let { lang, src } = item;
2287
+ resetCoverage();
2288
+ if (lang === "vue" || lang === "svelte") {
2289
+ const s = sfcSource(src);
2290
+ src = s.src;
2291
+ lang = s.lang;
2292
+ item = { ...item, src, lang };
2293
+ }
2294
+ const c = context(item);
792
2295
  if (borrowed) {
793
2296
  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 } : {}) });
2297
+ for (const cc of borrowed.calls) {
2298
+ const n = normCall(cc.name);
2299
+ if (!n) continue;
2300
+ if (cc.ref && n.sup) continue;
2301
+ const rec = { name: n.name, line: cc.line, ...(n.self ? { self: true } : {}), ...(n.sup ? { sup: true } : {}), ...(cc.ref ? { ref: true } : {}) };
2302
+ if (cc.args) rec.args = cc.args;
2303
+ if (cc.assign) rec.assign = cc.assign;
2304
+ calls.push(rec);
2305
+ }
2306
+ const defs = borrowed.defs.map((d) => ({ ...d, from: c.starts[d.s - 1] || 0 }));
2307
+ if (lang !== "py") {
2308
+ // A borrowed rung that is not Python still needs the ROUTE pass that
2309
+ // reads registrations out of the mask (`router.get("/p", …)`), because a
2310
+ // route handler is anonymous and the AST has no name to give it.
2311
+ const reg = new Set();
2312
+ const { defs: rdefs, regParens, exportsSet: hexports } = braceDefs(c, lang);
2313
+ resetCoverage();
2314
+ for (const d of rdefs) if (d.kind === "route") defs.push({ ...d, s: lineOf(c.starts, d.from), e: lineOf(c.starts, Math.max(d.from, d.to)) });
2315
+ for (const p of regParens) reg.add(lineOf(c.starts, p));
2316
+ for (const cc of calls) if (reg.has(cc.line) && ARGS_WORTH.test(cc.name)) cc.reg = true;
2317
+ defs.sort((a, b) => a.from - b.from);
2318
+ // `module.exports = { … }` is not in the AST walk; the heuristic half
2319
+ // that just ran read it out of the source, so the two sets are merged.
2320
+ const exp = new Set([...(borrowed.exportsSet || []), ...(hexports || [])]);
2321
+ return finalize(c, lang, defs, calls, borrowed.imports, exp.size ? exp : null, borrowed.extractor);
797
2322
  }
798
- return finalize(rel, lang, src, m, starts, strings, borrowed.defs, calls, borrowed.imports, null, borrowed.extractor);
2323
+ // Django routes are module-level calls the ast visitor reports with their
2324
+ // argument pieces; the heuristic finds them by regex. Same shape either way.
2325
+ for (const cc of calls) {
2326
+ if (cc.ref || !cc.args || !/^(path|re_path|url)$/.test(cc.name)) continue;
2327
+ const pos = cc.args.filter((a) => !a.kw);
2328
+ if (!pos[0] || pos[0].s == null || !pos[1] || pos[1].inc != null || !pos[1].id) continue;
2329
+ const p = pos[0].s.replace(/^\^/, "").replace(/\$$/, "");
2330
+ const line = cc.line;
2331
+ // The ast visitor already reported `views.x` as a ref on this line.
2332
+ defs.push({ kind: "route", method: "ANY", path: p.startsWith("/") ? p : "/" + p, head: cc.name, name: "route", s: line, e: cc.end || line, from: c.starts[line - 1] || 0, exported: false });
2333
+ }
2334
+ return finalize(c, lang, defs, calls, borrowed.imports, null, borrowed.extractor);
799
2335
  }
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);
2336
+ if (lang === "py" || lang === "rb") {
2337
+ const { defs, defParens, regParens } = lang === "py" ? pyDefs(c) : rbDefs(c);
2338
+ return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), null, HEURISTIC);
803
2339
  }
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);
2340
+ const { defs, defParens, regParens, exportsSet } = braceDefs(c, lang);
2341
+ braceDecorators(c, lang, defs);
2342
+ return finalize(c, lang, defs, scanCalls(c, defParens, regParens), importsFor(lang, c.src, c.m, c.starts), exportsSet, HEURISTIC);
806
2343
  }
807
2344
 
808
2345
  // ── the borrowed Python parser ──────────────────────────────────────────────
2346
+ // Emits the SAME rich call shape the heuristic builds (`args` pieces with
2347
+ // `s` / `id` / `kw` / `list` / `inc`, and `assign`), so `finalize` is one code
2348
+ // path for both rungs.
809
2349
  const PY_AST = String.raw`
810
2350
  import ast, sys, json
811
2351
  def dotted(n):
@@ -816,10 +2356,51 @@ def dotted(n):
816
2356
  if isinstance(n, ast.Name):
817
2357
  parts.append(n.id)
818
2358
  return ".".join(reversed(parts))
2359
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "super":
2360
+ parts.append("super")
2361
+ return ".".join(reversed(parts))
2362
+ # OrderService().create() -- a constructor call is the receiver (W1).
2363
+ if parts and isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id[:1].isupper():
2364
+ parts.append(n.func.id)
2365
+ return ".".join(reversed(parts))
2366
+ return None
2367
+ def sconst(n):
2368
+ if isinstance(n, ast.Constant) and isinstance(n.value, str):
2369
+ return n.value
2370
+ if isinstance(n, ast.JoinedStr):
2371
+ out = []
2372
+ for v in n.values:
2373
+ if isinstance(v, ast.Constant) and isinstance(v.value, str):
2374
+ out.append(v.value)
2375
+ else:
2376
+ out.append("{}")
2377
+ return "".join(out)
819
2378
  return None
2379
+ def piece(n, kw=None):
2380
+ p = {}
2381
+ if kw:
2382
+ p["kw"] = kw
2383
+ s = sconst(n)
2384
+ if s is not None:
2385
+ p["s"] = s
2386
+ return p
2387
+ if isinstance(n, (ast.List, ast.Tuple)):
2388
+ items = [sconst(e) for e in n.elts]
2389
+ p["list"] = [i for i in items if i is not None]
2390
+ return p
2391
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "include" and n.args:
2392
+ inc = sconst(n.args[0])
2393
+ if inc is not None:
2394
+ p["inc"] = inc
2395
+ return p
2396
+ d = dotted(n)
2397
+ if d:
2398
+ p["id"] = d
2399
+ return p
2400
+ WORTH = set("get post put patch delete head options fetch request open include_router register_blueprint mount path re_path url APIRouter Blueprint Router route add_url_rule json".split())
820
2401
  class V(ast.NodeVisitor):
821
2402
  def __init__(s):
822
- s.defs = []; s.calls = []; s.imports = []; s.stack = []
2403
+ s.defs = []; s.calls = []; s.imports = []; s.stack = []; s.assign_for = {}; s.skip = set()
823
2404
  def qual(s, name):
824
2405
  chain = []
825
2406
  for kind, n in reversed(s.stack):
@@ -828,19 +2409,65 @@ class V(ast.NodeVisitor):
828
2409
  chain.append(n)
829
2410
  chain.reverse()
830
2411
  return ".".join(chain + [name]), bool(chain)
2412
+ def decos(s, node):
2413
+ out = []
2414
+ for d in node.decorator_list:
2415
+ if isinstance(d, ast.Call):
2416
+ s.skip.add(id(d))
2417
+ name = dotted(d.func)
2418
+ if not name:
2419
+ continue
2420
+ args = [piece(a) for a in d.args] + [piece(k.value, k.arg) for k in d.keywords if k.arg]
2421
+ out.append({"name": name, "args": args})
2422
+ else:
2423
+ name = dotted(d)
2424
+ if name:
2425
+ out.append({"name": name})
2426
+ return out
831
2427
  def visit_ClassDef(s, node):
832
2428
  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("_")})
2429
+ bases = [b for b in (dotted(x) for x in node.bases) if b]
2430
+ d = {"name": node.name, "qname": q, "kind": "class", "s": node.lineno, "e": node.end_lineno or node.lineno, "exported": not node.name.startswith("_")}
2431
+ if bases:
2432
+ d["bases"] = [b.split(".")[-1] for b in bases][:8]
2433
+ dl = s.decos(node)
2434
+ if dl:
2435
+ d["decos"] = dl
2436
+ s.defs.append(d)
834
2437
  s.stack.append(("class", node.name)); s.generic_visit(node); s.stack.pop()
835
2438
  def visit_FunctionDef(s, node):
836
2439
  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("_")})
2440
+ d = {"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("_")}
2441
+ dl = s.decos(node)
2442
+ if dl:
2443
+ d["decos"] = dl
2444
+ s.defs.append(d)
838
2445
  s.stack.append(("function", node.name)); s.generic_visit(node); s.stack.pop()
839
2446
  visit_AsyncFunctionDef = visit_FunctionDef
2447
+ def visit_Assign(s, node):
2448
+ if isinstance(node.value, ast.Call) and len(node.targets) == 1:
2449
+ t = dotted(node.targets[0])
2450
+ if t:
2451
+ s.assign_for[id(node.value)] = t
2452
+ s.generic_visit(node)
2453
+ def visit_AnnAssign(s, node):
2454
+ if isinstance(node.value, ast.Call):
2455
+ t = dotted(node.target)
2456
+ if t:
2457
+ s.assign_for[id(node.value)] = t
2458
+ s.generic_visit(node)
840
2459
  def visit_Call(s, node):
841
2460
  name = dotted(node.func)
842
- if name:
843
- s.calls.append({"name": name, "line": node.lineno})
2461
+ if name and id(node) not in s.skip:
2462
+ rec = {"name": name, "line": node.lineno}
2463
+ last = name.split(".")[-1]
2464
+ if last in WORTH:
2465
+ rec["args"] = [piece(a) for a in node.args] + [piece(k.value, k.arg) for k in node.keywords if k.arg]
2466
+ rec["end"] = node.end_lineno or node.lineno
2467
+ t = s.assign_for.get(id(node))
2468
+ if t:
2469
+ rec["assign"] = t
2470
+ s.calls.append(rec)
844
2471
  for a in list(node.args) + [k.value for k in node.keywords]:
845
2472
  r = dotted(a)
846
2473
  if r:
@@ -868,6 +2495,650 @@ sys.stdout.buffer.write(json.dumps(out).encode("utf-8"))
868
2495
  `;
869
2496
 
870
2497
  let pythonProbe;
2498
+ // ── the borrowed TypeScript parser (v1.8.2 W6, G7) ──────────────────────────
2499
+ // The Python `ast` rung proved the pattern: if the PROJECT already has the
2500
+ // tool, borrow it — one batch, exact ranges, and no dependency for ORC.
2501
+ //
2502
+ // A project with `node_modules/typescript` gets an EXACT parse of its `.ts`,
2503
+ // `.tsx`, `.js` and `.jsx` files. It is loaded IN PROCESS (`require`) and only
2504
+ // `createSourceFile` is used — no `Program`, no type checker, because the graph
2505
+ // is a locator and a type checker would make an update cost minutes.
2506
+ //
2507
+ // Everything else is unchanged: `finalize` gets the same shapes the heuristic
2508
+ // builds, the mask still runs (aliases and effects are read from it), and
2509
+ // `ORC_GRAPH_NO_BORROW=1` forces the heuristic back.
2510
+ let tsProbe;
2511
+ function findTypeScript(root) {
2512
+ if (tsProbe !== undefined && tsProbe !== null && tsProbe.root === root) return tsProbe;
2513
+ if (tsProbe === null) return null;
2514
+ tsProbe = null;
2515
+ if (!root) return null;
2516
+ try {
2517
+ const lib = path.join(root, "node_modules", "typescript", "lib", "typescript.js");
2518
+ if (!fs.existsSync(lib)) return null;
2519
+ const ts = require(lib);
2520
+ if (!ts || typeof ts.createSourceFile !== "function") return null;
2521
+ tsProbe = { ts, ver: String(ts.version || "?"), root };
2522
+ } catch (_) {
2523
+ tsProbe = null;
2524
+ }
2525
+ return tsProbe;
2526
+ }
2527
+
2528
+ const TS_SCRIPT_KIND = (ts, rel) => {
2529
+ if (/\.tsx$/i.test(rel)) return ts.ScriptKind.TSX;
2530
+ if (/\.[cm]?ts$/i.test(rel)) return ts.ScriptKind.TS;
2531
+ if (/\.jsx$/i.test(rel)) return ts.ScriptKind.JSX;
2532
+ return ts.ScriptKind.JS;
2533
+ };
2534
+
2535
+ function tsWalk(ts, it) {
2536
+ const sf = ts.createSourceFile(it.rel, it.src, ts.ScriptTarget.Latest, true, TS_SCRIPT_KIND(ts, it.rel));
2537
+ const lineAt = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
2538
+ const defs = [];
2539
+ const calls = [];
2540
+ const imports = [];
2541
+ const exportsSet = new Set();
2542
+ const chain = []; // the enclosing CLASS names, innermost last
2543
+
2544
+ const text = (n) => {
2545
+ try {
2546
+ return n.getText(sf);
2547
+ } catch (_) {
2548
+ return "";
2549
+ }
2550
+ };
2551
+ const unwrap = (n) => {
2552
+ while (n && (ts.isParenthesizedExpression(n) || ts.isNonNullExpression(n) || ts.isAsExpression(n) || (ts.isTypeAssertionExpression && ts.isTypeAssertionExpression(n)))) n = n.expression;
2553
+ return n;
2554
+ };
2555
+ // A dotted NAME the resolver understands: `a.b.c`, `this.x`, `super.m`. A
2556
+ // constructor call as the receiver carries the CLASS as the head, which is
2557
+ // the W1-chained-new rule (`new S().m()` is a call on `S`).
2558
+ const dotted = (node) => {
2559
+ const n = unwrap(node);
2560
+ if (!n) return null;
2561
+ if (ts.isIdentifier(n) || ts.isPrivateIdentifier(n)) return n.text.replace(/^#/, "");
2562
+ if (n.kind === ts.SyntaxKind.ThisKeyword) return "this";
2563
+ if (n.kind === ts.SyntaxKind.SuperKeyword) return "super";
2564
+ if (ts.isPropertyAccessExpression(n)) {
2565
+ const head = dotted(n.expression);
2566
+ const tail = n.name && n.name.text;
2567
+ if (!tail) return null;
2568
+ // A receiver the parser cannot name (`request(app).get(…)`, `rows[0].id`)
2569
+ // leaves the BARE member name, exactly as the heuristic's chain regex
2570
+ // does — and W1's rule keeps a bare name from ever becoming a method
2571
+ // edge. Dropping the call instead would lose the URL it carries.
2572
+ return head ? `${head}.${tail}` : tail;
2573
+ }
2574
+ if (ts.isNewExpression(n)) {
2575
+ const c = dotted(n.expression);
2576
+ return c && /^[A-Z]/.test(c.split(".").pop()) ? c : null;
2577
+ }
2578
+ if (ts.isCallExpression(n)) {
2579
+ const c = dotted(n.expression);
2580
+ if (c === "super") return "super";
2581
+ return c && /^[A-Z]/.test(c.split(".").pop()) ? c : null;
2582
+ }
2583
+ return null;
2584
+ };
2585
+ // The piece shapes `argPieces` builds, so `urlOf`, `mountOf`, `prefixOf` and
2586
+ // `decoRoute` are ONE code path for the borrowed rung and the heuristic.
2587
+ const strOf = (n) => {
2588
+ const e = unwrap(n);
2589
+ if (!e) return null;
2590
+ if (ts.isStringLiteralLike(e)) return e.text;
2591
+ if (ts.isTemplateExpression(e)) return [e.head.text, ...e.templateSpans.map((s) => s.literal.text)].join("{}");
2592
+ return null;
2593
+ };
2594
+ const piece = (n, kw) => {
2595
+ const p = {};
2596
+ if (kw) p.kw = kw;
2597
+ const e = unwrap(n);
2598
+ const s = strOf(e);
2599
+ if (s != null) {
2600
+ p.s = s;
2601
+ return p;
2602
+ }
2603
+ if (ts.isArrayLiteralExpression(e)) {
2604
+ p.list = e.elements.map(strOf).filter((x) => x != null);
2605
+ return p;
2606
+ }
2607
+ if (ts.isObjectLiteralExpression(e)) {
2608
+ const obj = {};
2609
+ for (const pr of e.properties) {
2610
+ if (!ts.isPropertyAssignment(pr) || !pr.name) continue;
2611
+ const key = ts.isIdentifier(pr.name) || ts.isStringLiteralLike(pr.name) ? pr.name.text : null;
2612
+ const val = strOf(pr.initializer);
2613
+ if (key && val != null) obj[key] = val;
2614
+ }
2615
+ if (Object.keys(obj).length) p.obj = obj;
2616
+ return p;
2617
+ }
2618
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.PlusToken) {
2619
+ // `BASE + "/p"` — the TAIL is known, the prefix is not.
2620
+ const tail = strOf(e.right);
2621
+ if (tail != null) p.cat = tail;
2622
+ return p;
2623
+ }
2624
+ if (ts.isCallExpression(e)) {
2625
+ const head = dotted(e.expression);
2626
+ const first = e.arguments && e.arguments.length ? strOf(e.arguments[0]) : null;
2627
+ if (head === "require" && first != null) {
2628
+ p.req = first;
2629
+ return p;
2630
+ }
2631
+ }
2632
+ const id = dotted(e);
2633
+ if (id) p.id = id;
2634
+ return p;
2635
+ };
2636
+ const skip = new Set(); // decorator call expressions — never a call of the declaration
2637
+ const decosOf = (n) => {
2638
+ const list = (ts.getDecorators ? ts.getDecorators(n) : null) || (n.decorators || []);
2639
+ const out = [];
2640
+ for (const d of list) {
2641
+ const e = unwrap(d.expression);
2642
+ if (ts.isCallExpression(e)) {
2643
+ skip.add(e);
2644
+ const name = dotted(e.expression);
2645
+ if (!name) continue;
2646
+ out.push({ name, args: e.arguments.map((a) => piece(a)) });
2647
+ } else {
2648
+ const name = dotted(e);
2649
+ if (name) out.push({ name });
2650
+ }
2651
+ }
2652
+ return out;
2653
+ };
2654
+ const modifiers = (n) => (ts.getModifiers ? ts.getModifiers(n) : n.modifiers) || [];
2655
+ const has = (n, k) => modifiers(n).some((m2) => m2.kind === k);
2656
+ const isExported = (n) => has(n, ts.SyntaxKind.ExportKeyword);
2657
+ const isPrivate = (n) => has(n, ts.SyntaxKind.PrivateKeyword) || has(n, ts.SyntaxKind.ProtectedKeyword);
2658
+
2659
+ const push = (node, name, kind, extra) => {
2660
+ if (!name) return;
2661
+ defs.push({
2662
+ name,
2663
+ qname: chain.length && kind === "method" ? `${chain[chain.length - 1]}.${name}` : name,
2664
+ kind,
2665
+ s: lineAt(node.getStart(sf)),
2666
+ e: lineAt(node.getEnd()),
2667
+ ...extra,
2668
+ });
2669
+ };
2670
+
2671
+ const visit = (node) => {
2672
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
2673
+ const bindings = [];
2674
+ const cl = node.importClause;
2675
+ if (cl) {
2676
+ if (cl.name) bindings.push({ local: cl.name.text, orig: "default", kind: "default" });
2677
+ const nb = cl.namedBindings;
2678
+ if (nb && ts.isNamespaceImport(nb)) bindings.push({ local: nb.name.text, orig: "*", kind: "namespace" });
2679
+ else if (nb && ts.isNamedImports(nb)) for (const el of nb.elements) bindings.push({ local: el.name.text, orig: (el.propertyName || el.name).text, kind: "named" });
2680
+ }
2681
+ imports.push({ from: node.moduleSpecifier.text, line: lineAt(node.getStart(sf)), bindings });
2682
+ }
2683
+ if (ts.isExportDeclaration(node) && !node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
2684
+ for (const el of node.exportClause.elements) exportsSet.add(el.name.text);
2685
+ }
2686
+ if (ts.isExportAssignment(node)) {
2687
+ const d = dotted(node.expression);
2688
+ if (d) exportsSet.add(d);
2689
+ }
2690
+ if (ts.isClassDeclaration(node) && node.name) {
2691
+ const bases = [];
2692
+ for (const h of node.heritageClauses || []) for (const t of h.types) {
2693
+ const d = dotted(t.expression);
2694
+ if (d) bases.push(d.split(".").pop());
2695
+ }
2696
+ const decos = decosOf(node);
2697
+ push(node, node.name.text, "class", {
2698
+ exported: isExported(node),
2699
+ ...(bases.length ? { bases: bases.slice(0, 8) } : {}),
2700
+ ...(decos.length ? { decos } : {}),
2701
+ });
2702
+ chain.push(node.name.text);
2703
+ ts.forEachChild(node, visit);
2704
+ chain.pop();
2705
+ return;
2706
+ }
2707
+ if (ts.isFunctionDeclaration(node) && node.name && node.body) {
2708
+ push(node, node.name.text, "function", { exported: isExported(node) });
2709
+ }
2710
+ if (ts.isConstructorDeclaration(node) && node.body && chain.length) {
2711
+ push(node, "constructor", "method", { exported: true });
2712
+ }
2713
+ if ((ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) && node.body && node.name && chain.length) {
2714
+ const name = ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name) ? node.name.text : ts.isPrivateIdentifier(node.name) ? node.name.text.replace(/^#/, "") : null;
2715
+ const decos = decosOf(node);
2716
+ push(node, name, "method", { exported: !isPrivate(node), ...(decos.length ? { decos } : {}) });
2717
+ }
2718
+ // `handle = (req, res) => {}` on a class is a method; at module level it is
2719
+ // a function. An OVERLOAD signature and an `abstract` member have no body
2720
+ // and are not symbols — recording them made every `ctx` on them ambiguous.
2721
+ if (ts.isPropertyDeclaration(node) && node.initializer && node.name && chain.length) {
2722
+ const init = unwrap(node.initializer);
2723
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
2724
+ const name = ts.isIdentifier(node.name) ? node.name.text : null;
2725
+ const decos = decosOf(node);
2726
+ push(node, name, "method", { exported: !isPrivate(node), ...(decos.length ? { decos } : {}) });
2727
+ }
2728
+ }
2729
+ if (ts.isVariableStatement(node)) {
2730
+ for (const d of node.declarationList.declarations) {
2731
+ const init = d.initializer && unwrap(d.initializer);
2732
+ if (!init) continue;
2733
+ if (!chain.length && ts.isIdentifier(d.name) && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) push(d, d.name.text, "function", { exported: isExported(node) });
2734
+ // `const x = require("m")` · `const { a } = require("m")` ·
2735
+ // `const x = require("m").Y`
2736
+ let call = init;
2737
+ let prop = null;
2738
+ if (ts.isPropertyAccessExpression(call)) {
2739
+ prop = call.name.text;
2740
+ call = unwrap(call.expression);
2741
+ }
2742
+ if (!ts.isCallExpression(call)) continue;
2743
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") continue;
2744
+ const spec = call.arguments[0] && ts.isStringLiteralLike(call.arguments[0]) ? call.arguments[0].text : null;
2745
+ if (!spec) continue;
2746
+ const bindings = [];
2747
+ if (ts.isObjectBindingPattern(d.name)) {
2748
+ for (const el of d.name.elements) if (ts.isIdentifier(el.name)) bindings.push({ local: el.name.text, orig: (el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text), kind: "named" });
2749
+ } else if (ts.isIdentifier(d.name)) {
2750
+ if (prop) bindings.push({ local: d.name.text, orig: prop, kind: "named" });
2751
+ else bindings.push({ local: d.name.text, orig: "default", kind: "namespace" });
2752
+ }
2753
+ if (bindings.length) imports.push({ from: spec, line: lineAt(d.getStart(sf)), bindings });
2754
+ }
2755
+ }
2756
+ if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && !skip.has(node)) {
2757
+ const name = dotted(node.expression);
2758
+ // The line of the CALLEE's own name, not of the whole expression: in a
2759
+ // chain broken over lines (`request(app)\n .post("/orders")`) the call
2760
+ // a reader points at is `.post`, and that is the line the heuristic
2761
+ // reports and the line a card has to print.
2762
+ const callee = unwrap(node.expression);
2763
+ const at = callee && ts.isPropertyAccessExpression(callee) && callee.name ? callee.name : callee || node;
2764
+ const line = lineAt(at.getStart(sf));
2765
+ if (name) {
2766
+ // The RAW dotted name: `extractOne` normalises every borrowed rung's
2767
+ // calls in ONE place, and normalising twice loses `self`
2768
+ // (`this.ok` would already be `ok` by the time it got there).
2769
+ const rec = { name, line };
2770
+ if (ARGS_WORTH.test(name)) {
2771
+ rec.args = (node.arguments || []).map((a) => piece(a));
2772
+ rec.end = lineAt(node.getEnd());
2773
+ const par = node.parent;
2774
+ if (par && ts.isVariableDeclaration(par) && ts.isIdentifier(par.name)) rec.assign = par.name.text;
2775
+ else if (par && ts.isBinaryExpression(par) && par.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
2776
+ const t = dotted(par.left);
2777
+ if (t) rec.assign = t;
2778
+ }
2779
+ }
2780
+ calls.push(rec);
2781
+ }
2782
+ // A name passed AS an argument (`app.get("/p", handler)`) is a ref: an
2783
+ // edge only when this file defines or imports it (`finalize` decides).
2784
+ for (const a of node.arguments || []) {
2785
+ const r = dotted(a);
2786
+ if (r) calls.push({ name: r, line: lineAt(a.getStart(sf)), ref: true });
2787
+ }
2788
+ }
2789
+ ts.forEachChild(node, visit);
2790
+ };
2791
+ ts.forEachChild(sf, visit);
2792
+ // `module.exports = …` and `exports.x = …` are CommonJS, and the regex half
2793
+ // of the heuristic already reads them out of the source; the walker adds the
2794
+ // ES forms above. Both end up in the same set.
2795
+ for (const e of it.src.matchAll(/(?:^|[\s;])(?:module\.)?exports\.([A-Za-z_$][\w$]*)[ \t]*=/g)) exportsSet.add(e[1]);
2796
+ return { defs, calls, imports, exportsSet: [...exportsSet] };
2797
+ }
2798
+
2799
+ function runTsWalk(items, root) {
2800
+ const probe = findTypeScript(root);
2801
+ if (!probe) return null;
2802
+ const map = {};
2803
+ let ok = 0;
2804
+ for (const it of items) {
2805
+ try {
2806
+ map[it.rel] = tsWalk(probe.ts, it);
2807
+ ok++;
2808
+ } catch (_) {
2809
+ // Per FILE, exactly like the Python rung: one file the parser cannot
2810
+ // read falls back to the heuristic, and the rest of the batch is exact.
2811
+ map[it.rel] = null;
2812
+ }
2813
+ }
2814
+ return ok ? { map, extractor: `typescript@${probe.ver}` } : null;
2815
+ }
2816
+
2817
+ // ── the borrowed Go parser (v1.8.2 W6, G7) ──────────────────────────────────
2818
+ // Go ships its own parser in its standard library, so a repository that has a
2819
+ // Go toolchain can have an EXACT parse for free. The program below is written
2820
+ // once per CLI process to the OS temp directory and run with `go run` over the
2821
+ // whole batch — JSON in on stdin, JSON out on stdout, the same contract the
2822
+ // Python rung uses. No module, no network: it imports only `go/parser`,
2823
+ // `go/ast`, `go/token`, `encoding/json` and `os`.
2824
+ const GO_AST = String.raw`package main
2825
+
2826
+ import (
2827
+ "encoding/json"
2828
+ "go/ast"
2829
+ "go/parser"
2830
+ "go/token"
2831
+ "os"
2832
+ "strconv"
2833
+ "strings"
2834
+ )
2835
+
2836
+ type Piece struct {
2837
+ S *string ` + "`json:\"s,omitempty\"`" + `
2838
+ ID string ` + "`json:\"id,omitempty\"`" + `
2839
+ List []string ` + "`json:\"list,omitempty\"`" + `
2840
+ Cat string ` + "`json:\"cat,omitempty\"`" + `
2841
+ }
2842
+
2843
+ type Call struct {
2844
+ Name string ` + "`json:\"name\"`" + `
2845
+ Line int ` + "`json:\"line\"`" + `
2846
+ End int ` + "`json:\"end,omitempty\"`" + `
2847
+ Ref bool ` + "`json:\"ref,omitempty\"`" + `
2848
+ Assign string ` + "`json:\"assign,omitempty\"`" + `
2849
+ Args []Piece ` + "`json:\"args,omitempty\"`" + `
2850
+ }
2851
+
2852
+ type Def struct {
2853
+ Name string ` + "`json:\"name\"`" + `
2854
+ QName string ` + "`json:\"qname\"`" + `
2855
+ Kind string ` + "`json:\"kind\"`" + `
2856
+ S int ` + "`json:\"s\"`" + `
2857
+ E int ` + "`json:\"e\"`" + `
2858
+ Exported bool ` + "`json:\"exported\"`" + `
2859
+ Bases []string ` + "`json:\"bases,omitempty\"`" + `
2860
+ }
2861
+
2862
+ type Imp struct {
2863
+ From string ` + "`json:\"from\"`" + `
2864
+ Line int ` + "`json:\"line\"`" + `
2865
+ Bindings []Binding ` + "`json:\"bindings\"`" + `
2866
+ }
2867
+
2868
+ type Binding struct {
2869
+ Local string ` + "`json:\"local\"`" + `
2870
+ Orig string ` + "`json:\"orig\"`" + `
2871
+ Kind string ` + "`json:\"kind\"`" + `
2872
+ }
2873
+
2874
+ type Out struct {
2875
+ Defs []Def ` + "`json:\"defs\"`" + `
2876
+ Calls []Call ` + "`json:\"calls\"`" + `
2877
+ Imports []Imp ` + "`json:\"imports\"`" + `
2878
+ }
2879
+
2880
+ type Item struct {
2881
+ Rel string ` + "`json:\"rel\"`" + `
2882
+ Abs string ` + "`json:\"abs\"`" + `
2883
+ }
2884
+
2885
+ var worth = map[string]bool{
2886
+ "Get": true, "Post": true, "Put": true, "Patch": true, "Delete": true, "Head": true, "Options": true,
2887
+ "HandleFunc": true, "Handle": true, "GET": true, "POST": true, "PUT": true, "PATCH": true, "DELETE": true,
2888
+ "HEAD": true, "OPTIONS": true, "Any": true, "Mount": true, "Group": true, "NewRequest": true,
2889
+ "NewRequestWithContext": true, "Query": true, "Exec": true, "ExecContext": true, "QueryContext": true,
2890
+ "QueryRow": true, "QueryRowContext": true, "Getenv": true, "LookupEnv": true, "ReadFile": true,
2891
+ "WriteFile": true, "Open": true, "Create": true,
2892
+ }
2893
+
2894
+ func dotted(e ast.Expr) string {
2895
+ switch n := e.(type) {
2896
+ case *ast.Ident:
2897
+ return n.Name
2898
+ case *ast.SelectorExpr:
2899
+ h := dotted(n.X)
2900
+ if h == "" {
2901
+ return ""
2902
+ }
2903
+ return h + "." + n.Sel.Name
2904
+ case *ast.ParenExpr:
2905
+ return dotted(n.X)
2906
+ case *ast.StarExpr:
2907
+ return dotted(n.X)
2908
+ case *ast.IndexExpr:
2909
+ return dotted(n.X)
2910
+ case *ast.CallExpr:
2911
+ h := dotted(n.Fun)
2912
+ if h == "" {
2913
+ return ""
2914
+ }
2915
+ last := h[strings.LastIndex(h, ".")+1:]
2916
+ if last != "" && strings.ToUpper(last[:1]) == last[:1] {
2917
+ return h
2918
+ }
2919
+ return ""
2920
+ }
2921
+ return ""
2922
+ }
2923
+
2924
+ func strOf(e ast.Expr) (string, bool) {
2925
+ switch n := e.(type) {
2926
+ case *ast.BasicLit:
2927
+ if n.Kind == token.STRING {
2928
+ v, err := strconv.Unquote(n.Value)
2929
+ if err == nil {
2930
+ return v, true
2931
+ }
2932
+ }
2933
+ case *ast.BinaryExpr:
2934
+ if n.Op == token.ADD {
2935
+ if r, ok := strOf(n.Y); ok {
2936
+ return r, false
2937
+ }
2938
+ }
2939
+ }
2940
+ return "", false
2941
+ }
2942
+
2943
+ func pieceOf(e ast.Expr) Piece {
2944
+ p := Piece{}
2945
+ if s, whole := strOf(e); s != "" {
2946
+ if whole {
2947
+ v := s
2948
+ p.S = &v
2949
+ } else {
2950
+ p.Cat = s
2951
+ }
2952
+ return p
2953
+ }
2954
+ if cl, ok := e.(*ast.CompositeLit); ok {
2955
+ for _, el := range cl.Elts {
2956
+ if s, whole := strOf(el); whole {
2957
+ p.List = append(p.List, s)
2958
+ }
2959
+ }
2960
+ if len(p.List) > 0 {
2961
+ return p
2962
+ }
2963
+ }
2964
+ if id := dotted(e); id != "" {
2965
+ p.ID = id
2966
+ }
2967
+ return p
2968
+ }
2969
+
2970
+ func main() {
2971
+ var items []Item
2972
+ dec := json.NewDecoder(os.Stdin)
2973
+ if err := dec.Decode(&items); err != nil {
2974
+ os.Exit(1)
2975
+ }
2976
+ out := map[string]*Out{}
2977
+ for _, it := range items {
2978
+ fset := token.NewFileSet()
2979
+ f, err := parser.ParseFile(fset, it.Abs, nil, parser.SkipObjectResolution)
2980
+ if err != nil || f == nil {
2981
+ out[it.Rel] = nil
2982
+ continue
2983
+ }
2984
+ line := func(p token.Pos) int { return fset.Position(p).Line }
2985
+ o := &Out{Defs: []Def{}, Calls: []Call{}, Imports: []Imp{}}
2986
+ for _, im := range f.Imports {
2987
+ pth, err := strconv.Unquote(im.Path.Value)
2988
+ if err != nil {
2989
+ continue
2990
+ }
2991
+ local := pth[strings.LastIndex(pth, "/")+1:]
2992
+ alias := ""
2993
+ if im.Name != nil {
2994
+ alias = im.Name.Name
2995
+ }
2996
+ b := []Binding{}
2997
+ if alias != "_" && alias != "." {
2998
+ if alias != "" {
2999
+ local = alias
3000
+ }
3001
+ b = append(b, Binding{Local: local, Orig: local, Kind: "namespace"})
3002
+ }
3003
+ o.Imports = append(o.Imports, Imp{From: pth, Line: line(im.Pos()), Bindings: b})
3004
+ }
3005
+ for _, d := range f.Decls {
3006
+ switch n := d.(type) {
3007
+ case *ast.GenDecl:
3008
+ if n.Tok != token.TYPE {
3009
+ continue
3010
+ }
3011
+ for _, sp := range n.Specs {
3012
+ ts, ok := sp.(*ast.TypeSpec)
3013
+ if !ok {
3014
+ continue
3015
+ }
3016
+ def := Def{Name: ts.Name.Name, QName: ts.Name.Name, Kind: "class", S: line(ts.Pos()), E: line(ts.End()), Exported: ts.Name.IsExported()}
3017
+ // An EMBEDDED struct field is Go's inheritance: the outer
3018
+ // type answers the embedded type's methods.
3019
+ if st, ok := ts.Type.(*ast.StructType); ok && st.Fields != nil {
3020
+ for _, fl := range st.Fields.List {
3021
+ if len(fl.Names) == 0 {
3022
+ if b := dotted(fl.Type); b != "" {
3023
+ def.Bases = append(def.Bases, b[strings.LastIndex(b, ".")+1:])
3024
+ }
3025
+ }
3026
+ }
3027
+ }
3028
+ if it, ok := ts.Type.(*ast.InterfaceType); ok && it.Methods != nil {
3029
+ for _, fl := range it.Methods.List {
3030
+ if len(fl.Names) == 0 {
3031
+ if b := dotted(fl.Type); b != "" {
3032
+ def.Bases = append(def.Bases, b[strings.LastIndex(b, ".")+1:])
3033
+ }
3034
+ }
3035
+ }
3036
+ }
3037
+ o.Defs = append(o.Defs, def)
3038
+ }
3039
+ case *ast.FuncDecl:
3040
+ if n.Body == nil {
3041
+ continue
3042
+ }
3043
+ recv := ""
3044
+ if n.Recv != nil && len(n.Recv.List) > 0 {
3045
+ recv = dotted(n.Recv.List[0].Type)
3046
+ recv = recv[strings.LastIndex(recv, ".")+1:]
3047
+ }
3048
+ kind := "function"
3049
+ q := n.Name.Name
3050
+ if recv != "" {
3051
+ kind = "method"
3052
+ q = recv + "." + n.Name.Name
3053
+ }
3054
+ o.Defs = append(o.Defs, Def{Name: n.Name.Name, QName: q, Kind: kind, S: line(n.Pos()), E: line(n.End()), Exported: n.Name.IsExported()})
3055
+ }
3056
+ }
3057
+ assign := map[ast.Expr]string{}
3058
+ ast.Inspect(f, func(nd ast.Node) bool {
3059
+ if a, ok := nd.(*ast.AssignStmt); ok && len(a.Lhs) == 1 && len(a.Rhs) == 1 {
3060
+ if t := dotted(a.Lhs[0]); t != "" {
3061
+ assign[a.Rhs[0]] = t
3062
+ }
3063
+ }
3064
+ return true
3065
+ })
3066
+ ast.Inspect(f, func(nd ast.Node) bool {
3067
+ ce, ok := nd.(*ast.CallExpr)
3068
+ if !ok {
3069
+ return true
3070
+ }
3071
+ name := dotted(ce.Fun)
3072
+ if name != "" {
3073
+ c := Call{Name: name, Line: line(ce.Pos())}
3074
+ last := name[strings.LastIndex(name, ".")+1:]
3075
+ if worth[last] {
3076
+ c.End = line(ce.End())
3077
+ for _, a := range ce.Args {
3078
+ c.Args = append(c.Args, pieceOf(a))
3079
+ }
3080
+ if t, ok := assign[ce]; ok {
3081
+ c.Assign = t
3082
+ }
3083
+ }
3084
+ o.Calls = append(o.Calls, c)
3085
+ }
3086
+ for _, a := range ce.Args {
3087
+ if r := dotted(a); r != "" {
3088
+ o.Calls = append(o.Calls, Call{Name: r, Line: line(a.Pos()), Ref: true})
3089
+ }
3090
+ }
3091
+ return true
3092
+ })
3093
+ out[it.Rel] = o
3094
+ }
3095
+ enc := json.NewEncoder(os.Stdout)
3096
+ if err := enc.Encode(out); err != nil {
3097
+ os.Exit(1)
3098
+ }
3099
+ }
3100
+ `;
3101
+
3102
+ let goProbe;
3103
+ function findGo() {
3104
+ if (goProbe !== undefined) return goProbe;
3105
+ goProbe = null;
3106
+ const cmd = process.env.ORC_GRAPH_GO || "go";
3107
+ try {
3108
+ const r = spawnSync(cmd, ["version"], { encoding: "utf8", timeout: 15000, windowsHide: true });
3109
+ if (r.status !== 0 || !/go\d|go version/.test(r.stdout || "")) return null;
3110
+ const ver = /go(\d+\.\d+(?:\.\d+)?)/.exec(r.stdout || "");
3111
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "orc-graph-go-"));
3112
+ fs.writeFileSync(path.join(dir, "main.go"), GO_AST);
3113
+ fs.writeFileSync(path.join(dir, "go.mod"), "module orcgraph\n\ngo 1.20\n");
3114
+ goProbe = { cmd, ver: ver ? ver[1] : "?", dir };
3115
+ } catch (_) {
3116
+ goProbe = null;
3117
+ }
3118
+ return goProbe;
3119
+ }
3120
+
3121
+ function runGoAst(items, root) {
3122
+ void root;
3123
+ const go = findGo();
3124
+ if (!go) return null;
3125
+ const r = spawnSync(go.cmd, ["run", "."], {
3126
+ cwd: go.dir,
3127
+ input: JSON.stringify(items.map((i) => ({ rel: i.rel, abs: i.abs }))),
3128
+ encoding: "utf8",
3129
+ maxBuffer: 512 * 1024 * 1024,
3130
+ timeout: 600000,
3131
+ windowsHide: true,
3132
+ env: { ...process.env, GOFLAGS: "-mod=mod", GO111MODULE: "on" },
3133
+ });
3134
+ if (r.status !== 0 || !r.stdout) return null;
3135
+ try {
3136
+ return { map: JSON.parse(r.stdout), extractor: `go-ast@${go.ver}` };
3137
+ } catch (_) {
3138
+ return null;
3139
+ }
3140
+ }
3141
+
871
3142
  function findPython() {
872
3143
  if (pythonProbe !== undefined) return pythonProbe;
873
3144
  pythonProbe = null;
@@ -907,14 +3178,32 @@ function runPythonAst(items) {
907
3178
  }
908
3179
 
909
3180
  // items: [{ rel, abs, lang, src }] → Map<rel, { extractor, imports, symbols }>
910
- function extractBatch(items) {
3181
+ function extractBatch(items, opts) {
911
3182
  const out = new Map();
3183
+ const root = (opts && opts.root) || null;
3184
+ const borrow = !process.env.ORC_GRAPH_NO_BORROW;
912
3185
  const pys = items.filter((i) => i.lang === "py");
913
- const ast = pys.length && !process.env.ORC_GRAPH_NO_BORROW ? runPythonAst(pys) : null;
3186
+ const ast = pys.length && borrow ? runPythonAst(pys) : null;
3187
+ // W6 (G7): TypeScript and JavaScript, when the PROJECT has the compiler.
3188
+ // The `<script>` block of an SFC is handled by the heuristic rung — the
3189
+ // walker would need the blanked source, and the gain is in the .ts tree.
3190
+ const jts = items.filter((i) => i.lang === "ts" || i.lang === "js");
3191
+ const tsw = jts.length && borrow ? runTsWalk(jts, root) : null;
3192
+ const gos = items.filter((i) => i.lang === "go");
3193
+ const goa = gos.length && borrow ? runGoAst(gos, root) : null;
3194
+ // Which rung answered for this file, so the record's `extractor` names the
3195
+ // tool that actually read it — a claim of `typescript@5.4.2` on a file the
3196
+ // walker choked on would be a lie the card repeats.
3197
+ const rungOf = (rel) => {
3198
+ if (ast && ast.map[rel]) return [ast.map[rel], ast.extractor];
3199
+ if (tsw && tsw.map[rel]) return [tsw.map[rel], tsw.extractor];
3200
+ if (goa && goa.map[rel]) return [goa.map[rel], goa.extractor];
3201
+ return [null, null];
3202
+ };
914
3203
  for (const it of items) {
915
- const b = ast && ast.map[it.rel];
3204
+ const [b, tag] = rungOf(it.rel);
916
3205
  try {
917
- out.set(it.rel, extractOne(it, b ? { ...b, extractor: ast.extractor } : null));
3206
+ out.set(it.rel, extractOne(it, b ? { ...b, extractor: tag } : null));
918
3207
  } catch (_) {
919
3208
  // A file the extractor cannot read is an empty record, never a failed
920
3209
  // update: one pathological file must not take the whole map down.
@@ -924,4 +3213,4 @@ function extractBatch(items) {
924
3213
  return out;
925
3214
  }
926
3215
 
927
- module.exports = { HEURISTIC, MAX_CALLS_PER_SYMBOL, MAX_EFFECTS_PER_SYMBOL, extractBatch, extractOne, mask, normCall, _findPython: findPython };
3216
+ module.exports = { HEURISTIC, MAX_CALLS_PER_SYMBOL, MAX_EFFECTS_PER_SYMBOL, extractBatch, extractOne, mask, normCall, urlText, joinPath, _findPython: findPython, _findTypeScript: findTypeScript, _findGo: findGo };