fix(extract): source calls to the enclosing callable for all grammars

The unified/calls extractor used a private 4-case name resolver
(resolve_func_name_node) that returned NULL for the ~130 grammars whose
function node has no `name` field — Fortran subroutine, SCSS mixin, SQL
create_function, Julia short-form, the Lisp/FP family, and more. On NULL,
push_boundary_scopes never pushed a SCOPE_FUNC, so every call inside such
a function was attributed to the enclosing Module instead of the callable
(QUALITY_ANALYSIS gap #3).

Export the rich resolver from extract_defs (cbm_resolve_func_name) — a
strict superset that already drives definition extraction (generic name
field, arrow/declarator, C/C++ declarator chain, plus the per-language
quirks) — and route the calls scope path (compute_func_qn) through it,
preserving the enclosing-class QN join. Definition extraction is
unchanged (it already called this resolver); the two paths now share one
source of truth, the same de-drift pattern used for
cbm_resolve_c_declarator_name_node (#438).

Reproduced by repro_invariant_enclosing_parity (Fortran, SCSS, SQL,
Verilog, Julia, Nix, CommonLisp, EmacsLisp, Dart, COBOL): each asserts no
CALLS edge is Module-sourced and was RED; the fix makes them
callable-sourced.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-06-27 00:46:54 +02:00
parent 639bac9c5d
commit 6e6b34a9bb
4 changed files with 24 additions and 29 deletions
+6 -5
View File
@@ -558,8 +558,9 @@ static TSNode find_first_descendant_by_kind(TSNode node,
return null_node;
}
// Forward declaration for mutual recursion.
static TSNode resolve_func_name(TSNode node, CBMLanguage lang);
// Forward declaration for mutual recursion. Exported (see helpers.h) so the
// unified/calls extractor shares this one resolver — see cbm_resolve_func_name.
TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
static bool is_cpp_template_inner_kind(const char *kind) {
return strcmp(kind, "function_definition") == 0 || strcmp(kind, "declaration") == 0 ||
@@ -639,7 +640,7 @@ static TSNode resolve_func_name_c_family(TSNode *node_ptr, CBMLanguage lang, con
// Resolve the name node for a function, handling language-specific quirks.
// Uses a loop to handle template_declaration unwrapping (avoids recursion).
static TSNode resolve_func_name(TSNode node, CBMLanguage lang) {
TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) {
enum { MAX_TEMPLATE_DEPTH = 2 };
for (int tmpl_depth = 0; tmpl_depth < MAX_TEMPLATE_DEPTH; tmpl_depth++) {
const char *kind = ts_node_type(node);
@@ -2671,7 +2672,7 @@ static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source)
static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
CBMArena *a = ctx->arena;
TSNode name_node = resolve_func_name(node, ctx->language);
TSNode name_node = cbm_resolve_func_name(node, ctx->language);
if (ts_node_is_null(name_node)) {
return;
}
@@ -3457,7 +3458,7 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) {
if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA ||
lang == CBM_LANG_GLSL) &&
strcmp(ck, "function_definition") == 0) {
return resolve_func_name(child, lang);
return cbm_resolve_func_name(child, lang);
}
if (lang == CBM_LANG_GROOVY && strcmp(ck, "function_definition") == 0) {
+7 -22
View File
@@ -87,27 +87,6 @@ static const char *compute_wolfram_func_qn(CBMExtractCtx *ctx, TSNode node) {
return NULL;
}
// Resolve the name node for a function, handling arrow functions.
static TSNode resolve_func_name_node(TSNode node) {
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
if (ts_node_is_null(name_node) && strcmp(ts_node_type(node), "arrow_function") == 0) {
TSNode parent = ts_node_parent(node);
if (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "variable_declarator") == 0) {
name_node = ts_node_child_by_field_name(parent, TS_FIELD("name"));
}
}
/* Grammars without a `name` field (e.g. newer tree-sitter-kotlin): the
* function name is a simple_identifier child of function_declaration. */
if (ts_node_is_null(name_node) && strcmp(ts_node_type(node), "function_declaration") == 0) {
name_node = cbm_find_child_by_kind(node, "simple_identifier");
}
/* C/C++/CUDA/GLSL: function_definition name lives in the declarator chain. */
if (ts_node_is_null(name_node) && strcmp(ts_node_type(node), "function_definition") == 0) {
name_node = cbm_resolve_c_declarator_name_node(node);
}
return name_node;
}
// Compute function QN for scope tracking (mirrors cbm_enclosing_func_qn logic).
static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
WalkState *state) {
@@ -116,7 +95,13 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
return compute_wolfram_func_qn(ctx, node);
}
TSNode name_node = resolve_func_name_node(node);
/* Resolve the function name via the single shared resolver (extract_defs) so
* call-scope attribution agrees with definition extraction across all ~130
* grammars. The old private 4-case copy returned NULL for Fortran subroutine,
* SCSS mixin, SQL create_function, Julia short-form, etc., so
* push_boundary_scopes never pushed a SCOPE_FUNC and the calls inside were
* mis-attributed to the enclosing Module (QUALITY_ANALYSIS gap #3). */
TSNode name_node = cbm_resolve_func_name(node, ctx->language);
if (ts_node_is_null(name_node)) {
return NULL;
}
+9
View File
@@ -50,6 +50,15 @@ const char *cbm_enclosing_func_qn_cached(CBMExtractCtx *ctx, TSNode node);
// enclosing-function attribution — drift between private copies caused #438.
TSNode cbm_resolve_c_declarator_name_node(TSNode func_node);
// Resolve a function/method definition node's NAME node across all ~130 grammars
// (generic `name` field, arrow→declarator, C/C++ declarator chain, plus the many
// per-language quirks: Fortran subroutine, SCSS mixin, SQL create_function, R,
// PowerShell, Ada, the Lisp/FP family, etc.). Defined in extract_defs.c. Shared by
// the defs, calls, and unified extractors so all three agree on enclosing-function
// naming — drift between private copies caused the Module-mis-attribution of
// gap #3 (and #438 for the C-declarator case).
TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
// Find a child node by kind string.
TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);
+2 -2
View File
@@ -507,7 +507,7 @@ static const char *elixir_var_types[] = {"binary_operator", NULL};
// ==================== HASKELL ====================
/* "bind" = a nullary value binding (`foo = 1`); has a `name` field like `function`.
* `signature` (type annotations) is suppressed in resolve_func_name so it never doubles. */
* `signature` (type annotations) is suppressed in cbm_resolve_func_name so it never doubles. */
static const char *haskell_func_types[] = {"function", "signature", "bind", NULL};
static const char *haskell_class_types[] = {"class", "data_type", "newtype", NULL};
static const char *haskell_module_types[] = {"haskell", NULL};
@@ -701,7 +701,7 @@ static const char *clojure_call_types[] = {"list_lit", NULL};
/* Top-level `let f () = ...` parses to function_or_value_defn (module-level
* value_declaration is aliased to declaration_expression, which wraps it). The
* name lives on a function_declaration_left/value_declaration_left child — see
* the CBM_LANG_FSHARP branch in resolve_func_name. */
* the CBM_LANG_FSHARP branch in cbm_resolve_func_name. */
static const char *fsharp_func_types[] = {"function_declaration", "value_declaration",
"function_or_value_defn", NULL};
static const char *fsharp_class_types[] = {"type_definition", "exception_definition", NULL};