feat(resolution): Erlang behaviour-callback dispatch synthesizer (#635, #648) (#1166)

Bridges the OTP callback boundary: a framework call through a variable
module — cowboy's Handler:init / Middleware:execute folds, a plugin
manager's Mod:callback(...) — now links to the repo's implementers of the
behaviour declaring that callback, so codegraph_explore connects flows
end-to-end across behaviour dispatch instead of stopping at it.

Precision gates: the callback arity must match the site, exactly one
in-repo behaviour may declare that (name, arity) — a collision bails
(cowboy's init/2 is declared by five handler-flavored behaviours and
correctly stays silent) — the implementer must export the callback, and
above the fan-out cap the site is skipped entirely (ejabberd's gen_mod
with ~230 implementers stays a visibly dynamic boundary). Behaviour
discovery scans -callback declarations in every module so implementer-less
behaviours still gate ambiguity. Edges carry provenance:'heuristic' with
synthesizedBy:'erlang-behaviour' and the wiring site, rendered as dynamic
dispatch in explore.

Validated per the dispatch-family playbook: cowboy 38 edges (middleware
chain, stream-handler folds, sub-protocol upgrade), ejabberd 598, emqx 843;
36/36 sampled edges precise (target declares the via-behaviour and exports
the callback); node counts unchanged; ~1.4s added on emqx's 2,273 files;
zero-control clean. The cowboy request flow connects in one explore call.

Includes an Erlang comment stripper (%-comments, string/atom/$-char aware)
for the dispatch-site scans.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-03 14:53:04 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 6511722250
commit 2217a35943
5 changed files with 429 additions and 1 deletions
+56 -1
View File
@@ -35,7 +35,8 @@ export type CommentLang =
| 'go'
| 'rust'
| 'c'
| 'cpp';
| 'cpp'
| 'erlang';
export function stripCommentsForRegex(content: string, lang: CommentLang): string {
switch (lang) {
@@ -45,6 +46,8 @@ export function stripCommentsForRegex(content: string, lang: CommentLang): strin
return stripRuby(content);
case 'rust':
return stripRust(content);
case 'erlang':
return stripErlang(content);
case 'php':
return stripPhp(content);
case 'go':
@@ -471,3 +474,55 @@ function stripRust(src: string): string {
return out.join('');
}
// ---------- Erlang ----------
/**
* Erlang: `%` starts a line comment unless it sits inside a `"string"`, a
* `'quoted atom'`, or is the character literal `$%`. Strings and quoted atoms
* are left intact (a behaviour callback name can be a quoted atom); only the
* comment text is blanked.
*/
function stripErlang(src: string): string {
const out = src.split('');
let i = 0;
const n = src.length;
while (i < n) {
const c = src[i];
if (c === '"' || c === "'") {
const quote = c;
i++;
while (i < n && src[i] !== quote) {
if (src[i] === '\\' && i + 1 < n) {
i += 2;
continue;
}
i++;
}
if (i < n) i++;
continue;
}
// Character literal: `$x`, `$\n`, `$%` — the next char (or escape) is data.
if (c === '$') {
i++;
if (i < n && src[i] === '\\') i++;
i++;
continue;
}
if (c === '%') {
let end = i;
while (end < n && src[end] !== '\n') end++;
blankRange(out, i, end, src);
i = end;
continue;
}
i++;
}
return out.join('');
}