@azure-id/orc 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +386 -0
- package/README-id.md +110 -73
- package/README.md +96 -33
- package/bin/cli.js +45520 -44867
- package/bin/graph-extract.js +2409 -120
- package/bin/graph-gain.js +404 -0
- package/bin/graph-map.js +232 -0
- package/bin/graph-notes.js +49 -8
- package/bin/graph-query.js +1770 -808
- package/bin/graph-resolve.js +93 -16
- package/bin/graph-shard.js +325 -0
- package/bin/graph.js +658 -605
- package/bin/verify-contracts.js +297 -56
- package/bin/verify-package.js +29 -1
- package/bin/webui/api.js +6 -0
- package/bin/webui/fixtures/index.js +6 -1
- package/bin/webui/fixtures/knowledge.js +41 -1
- package/bin/webui/fixtures/stats.js +107 -104
- package/bin/webui/i18n/en/knowledge.json +16 -1
- package/bin/webui/i18n/id/knowledge.json +16 -1
- package/bin/webui/js/panels/knowledge.js +68 -3
- package/mock-run/orc-quick.md +141 -113
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +15 -5
- package/templates/agents/orc-executor-haiku-4-5.md +25 -13
- package/templates/agents/orc-executor-opus-4-7-high.md +25 -13
- package/templates/agents/orc-executor-opus-4-7-med.md +25 -13
- package/templates/agents/orc-executor-opus-4-8-high.md +25 -13
- package/templates/agents/orc-executor-opus-5-high.md +25 -13
- package/templates/agents/orc-executor-opus-5-low.md +25 -13
- package/templates/agents/orc-executor-opus-5-med.md +25 -13
- package/templates/agents/orc-executor-sonnet-4-6-high.md +25 -13
- package/templates/agents/orc-executor-sonnet-4-6-med.md +25 -13
- package/templates/agents/orc-executor-sonnet-5-high.md +25 -13
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +15 -12
- package/templates/agents/orc-planner-mini-opus-5-med.md +75 -69
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +73 -67
- package/templates/agents/orc-recon-opus-5-low.md +99 -0
- package/templates/agents/orc-recon-sonnet-4-6-med.md +99 -0
- package/templates/commands/orc-mini.md +10 -12
- package/templates/commands/orc-quick.md +20 -33
- package/templates/hooks/README.md +13 -3
- package/templates/hooks/orc-graph-hook.js +148 -13
- package/templates/hooks/orc-trace.js +476 -471
- package/templates/skills/_shared/code-graph.md +148 -20
- package/templates/skills/_shared/phases/execution.md +13 -11
- package/templates/skills/_shared/phases/planning.md +8 -1
- package/templates/skills/_shared/phases/rules.md +172 -159
- package/templates/skills/_shared/phases/ship.md +5 -1
- package/templates/skills/_shared/phases/trace.md +4 -1
- package/templates/skills/_shared/phases/wiki-consult.md +10 -6
- package/templates/skills/_shared/read-ladder.md +10 -2
- package/templates/skills/_shared/return-validation.md +22 -0
- package/templates/skills/context-combiner/SKILL.md +13 -13
- package/templates/skills/orc/SKILL.md +1 -1
- package/templates/skills/orc/subskills/orc-execution/core.md +171 -159
- package/templates/skills/orc-analyze/SKILL.md +13 -13
- package/templates/skills/orc-diy/references/flow-schema.md +1 -1
- package/templates/skills/orc-mini/SKILL.md +148 -136
- package/templates/skills/orc-mini/examples/mini-run-mock.md +64 -50
- package/templates/skills/orc-mini/references/complexity.md +105 -0
- package/templates/skills/orc-quick/README.md +495 -423
- package/templates/skills/orc-quick/SKILL.md +157 -211
- package/templates/skills/orc-quick/references/context-doc.md +145 -114
- package/templates/skills/orc-quick/references/defect.md +101 -0
- package/templates/skills/orc-quick/references/dispatch-gate.md +55 -24
- package/templates/skills/orc-quick/references/gh-mode.md +148 -127
- package/templates/skills/orc-quick/references/look.md +107 -0
- package/templates/skills/orc-wiki/references/staleness.md +1 -1
package/bin/graph-extract.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
138
|
-
blank(
|
|
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(
|
|
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 ||
|
|
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
|
|
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$]*)[^{;]
|
|
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
|
|
225
|
-
classes.push(
|
|
226
|
-
add(
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 >=
|
|
295
|
-
if (body && body.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 (!
|
|
731
|
+
if (!MEMBER_KW.has(name)) {
|
|
300
732
|
const priv = /private|protected/.test(x[1]) || x[2].startsWith("#");
|
|
301
|
-
add({ name, qname: `${
|
|
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*)[^{;]
|
|
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
|
|
336
|
-
classes.push(
|
|
337
|
-
add(
|
|
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) ||
|
|
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*)[^{;]
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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(
|
|
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
|
|
428
|
-
const hit = d ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
492
|
-
if (!
|
|
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 (
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
524
|
-
if (
|
|
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,
|
|
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 =
|
|
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
|
|
690
|
-
if (HTTP_RE.test(
|
|
691
|
-
const p = parenFor(src, starts,
|
|
692
|
-
const url = p != null ? stringAt(src,
|
|
693
|
-
out.push({ type: "http", text: url ? `${
|
|
694
|
-
} else if (DB_RE.test(
|
|
695
|
-
out.push({ type: "db", text:
|
|
696
|
-
} else if (fsRe && fsRe.test(
|
|
697
|
-
out.push({ type: "fs", text:
|
|
698
|
-
} else if (lang === "py" &&
|
|
699
|
-
const p = parenFor(src, starts,
|
|
700
|
-
if (p != null && /^[^)]*?,\s*(?:mode\s*=\s*)?['"][wax]/.test(src.slice(p + 1, p + 120))) out.push({ type: "fs", text: "open (write)", 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(
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
|
739
|
-
|
|
740
|
-
|
|
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({
|
|
743
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2247
|
+
function context(item) {
|
|
788
2248
|
const { rel, lang, src } = item;
|
|
789
|
-
resetCoverage();
|
|
790
2249
|
const { masked: m, strings } = mask(src, lang);
|
|
791
|
-
const
|
|
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
|
|
795
|
-
const n = normCall(
|
|
796
|
-
if (n)
|
|
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
|
-
|
|
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(
|
|
802
|
-
return finalize(
|
|
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(
|
|
805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
3204
|
+
const [b, tag] = rungOf(it.rel);
|
|
916
3205
|
try {
|
|
917
|
-
out.set(it.rel, extractOne(it, b ? { ...b, extractor:
|
|
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 };
|