From 6e6b34a9bbaa1919f8964d98e513ee82afa6c3ce Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 27 Jun 2026 00:46:54 +0200 Subject: [PATCH] fix(extract): source calls to the enclosing callable for all grammars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/cbm/extract_defs.c | 11 ++++++----- internal/cbm/extract_unified.c | 29 +++++++---------------------- internal/cbm/helpers.h | 9 +++++++++ internal/cbm/lang_specs.c | 4 ++-- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 7e7fd5fd6..f5aa4b77d 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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) { diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index f65a64bec..676053439 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -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; } diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 35d108920..caa8e9097 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -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); diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 26d25b3d8..0c3192fea 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -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};