Calls hidden inside -define bodies were invisible: the extractor consumed pp_define without walking the replacement, and macro use sites produced no edges, so a call path routed through a macro (ejabberd's SQL upsert macros, logging wrappers) was completely dark. The macro's constant node now participates in the graph. The -define body's calls are attributed to the MACRO — true exactly once, instead of a per-use duplicate that would explode on logging macros — and each use site links in: ?MACRO(...) with arguments emits a `calls` ref (inlined code joins the call chain), a bare ?CONSTANT read emits `references` (answering "where is this macro used" without polluting call paths). Compiler-predefined macros (?MODULE, ?LINE, ?FUNCTION_NAME, ...) are excluded, macro-use arguments keep walking so a call nested in ?assertEqual(ok, do_thing()) still attributes to the enclosing function, and macro-to-macro chains connect. Validated: node counts unchanged on cowboy/ejabberd/emqx; edges +26/+7.3K/ +42K with honest hub shapes (?T i18n, ?SLOG logging, ?QOS_1 protocol constants); 40/40 sampled edges precise; +1.3s index cost on emqx's 2,273 files. The payoff chain on ejabberd: set_password_scram_t → ?SQL_UPSERT_T → ejabberd_sql:sql_query_t — database writes through SQL macros now trace end-to-end. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
7e3d44fa96
commit
a5b8cd8e25
@@ -175,12 +175,24 @@ function handleTypeAlias(node: SyntaxNode, ctx: ExtractorContext): boolean {
|
||||
function handlePpDefine(node: SyntaxNode, ctx: ExtractorContext): boolean {
|
||||
const lhs = getChildByField(node, 'lhs');
|
||||
const nameNode = lhs ? getChildByField(lhs, 'name') : null;
|
||||
if (nameNode) {
|
||||
ctx.createNode('constant', getNodeText(nameNode, ctx.source), node, {
|
||||
signature: collapseWs(getNodeText(node, ctx.source)).slice(0, 200),
|
||||
});
|
||||
if (!nameNode) return true;
|
||||
const macro = ctx.createNode('constant', getNodeText(nameNode, ctx.source), node, {
|
||||
signature: collapseWs(getNodeText(node, ctx.source)).slice(0, 200),
|
||||
});
|
||||
// The replacement's calls execute at expansion sites, but attributing them
|
||||
// to the MACRO node keeps them true exactly once: `-define(LOG_AUDIT(E),
|
||||
// audit_logger:log(E))` gives the LOG_AUDIT constant a `calls` edge to the
|
||||
// logger, and each `?LOG_AUDIT(...)` use site links to the constant (see the
|
||||
// macro_call_expr case in extractCall) — so the chain
|
||||
// `caller → LOG_AUDIT → audit_logger:log` traverses without minting a
|
||||
// per-use duplicate of the body's calls.
|
||||
const replacement = getChildByField(node, 'replacement');
|
||||
if (macro && replacement) {
|
||||
ctx.pushScope(macro.id);
|
||||
ctx.visitFunctionBody(replacement, macro.id);
|
||||
ctx.popScope();
|
||||
}
|
||||
return true; // the replacement's calls only exist at expansion sites
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleBehaviour(node: SyntaxNode, ctx: ExtractorContext): boolean {
|
||||
@@ -218,6 +230,7 @@ export const erlangExtractor: LanguageExtractor = {
|
||||
'record_update_expr', // X#rec{...}
|
||||
'record_index_expr', // #rec.field
|
||||
'record_field_expr', // X#rec.field
|
||||
'macro_call_expr', // ?MACRO / ?MACRO(...) — links use sites to the -define constant
|
||||
],
|
||||
variableTypes: [],
|
||||
nameField: 'name',
|
||||
|
||||
Reference in New Issue
Block a user