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',
|
||||
|
||||
@@ -67,6 +67,13 @@ const VUE_STORE_FILE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutati
|
||||
* Used by the erlang branch of extractCall to lift a static MFA pair into a
|
||||
* call edge (the spawned/applied function is otherwise invisible to the graph).
|
||||
*/
|
||||
/** Compiler-predefined Erlang macros — no `-define` exists to link a use to. */
|
||||
const ERLANG_PREDEFINED_MACROS = new Set([
|
||||
'MODULE', 'MODULE_STRING', 'FILE', 'LINE', 'MACHINE',
|
||||
'FUNCTION_NAME', 'FUNCTION_ARITY', 'OTP_RELEASE',
|
||||
'FEATURE_AVAILABLE', 'FEATURE_ENABLED',
|
||||
]);
|
||||
|
||||
const ERLANG_MFA_CALLS = new Set([
|
||||
'spawn', 'spawn_link', 'spawn_monitor', 'spawn_opt', 'apply',
|
||||
'erlang:spawn', 'erlang:spawn_link', 'erlang:spawn_monitor', 'erlang:spawn_opt', 'erlang:apply',
|
||||
@@ -3752,6 +3759,29 @@ export class TreeSitterExtractor {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (node.type === 'macro_call_expr') {
|
||||
// Macro use site → the `-define` constant node. Function-like uses
|
||||
// (`?LOG_AUDIT(X)` — args present) are inlined code, so they join the
|
||||
// call chain and connect through the macro node to the body's calls
|
||||
// (attributed there by handlePpDefine); bare reads (`?TIMEOUT`) are
|
||||
// `references`, answering "where is this macro used" without
|
||||
// polluting call paths. Compiler-predefined macros carry no
|
||||
// definition to link. The use site's ARGUMENTS are children and keep
|
||||
// walking, so a call nested in `?assertEqual(ok, do_thing())` still
|
||||
// attributes to the enclosing function.
|
||||
const macroName = getChildByField(node, 'name');
|
||||
if (!macroName) return;
|
||||
const name = getNodeText(macroName, this.source);
|
||||
if (ERLANG_PREDEFINED_MACROS.has(name)) return;
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: name,
|
||||
referenceKind: getChildByField(node, 'args') ? 'calls' : 'references',
|
||||
line,
|
||||
column,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// record_expr / record_update_expr / record_index_expr / record_field_expr
|
||||
const recordName = getChildByField(node, 'name');
|
||||
const recordAtom = recordName?.type === 'record_name' ? getChildByField(recordName, 'name') : null;
|
||||
|
||||
Reference in New Issue
Block a user