From a044a71d59795480b7f57d071fcf9d9a0ec37d25 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 27 Jun 2026 02:15:52 +0200 Subject: [PATCH] fix(extract): def-name + callee extraction for scss/sql/cobol/elm/teal Several grammars produced no Function def and/or no CALLS edge because the name/callee is not in the standard `name`/`function` field. Add precise per-grammar extraction (verified against the actual tree-sitter parse trees): - SCSS: function_statement/mixin_statement name is a `name` CHILD (not a field); the call node is `include_statement` (callee = its identifier child). Also fix scss_call_types, which listed "call_expression" instead of include_statement. - SQL: create_function / invocation carry the name on a nested object_reference > `name` field. - COBOL: call_statement's callee is its `x` string-literal (program name), dequoted. - Elm: value_declaration name is functionDeclarationLeft > lower_case_identifier; function_call_expr callee is target > value_expr > name (value_qid). - Teal: the `local function foo()` form carries the name on a `function_name` child rather than the `name` field. Reproduced by the repro_grammar_* and repro_invariant_enclosing_parity suites (SCSS/SQL/COBOL/Elm/Teal), which were RED for missing defs/calls. Signed-off-by: Martin Vogel --- internal/cbm/extract_calls.c | 90 ++++++++++++++++++++++++++++++++++++ internal/cbm/extract_defs.c | 51 ++++++++++++++++++++ internal/cbm/lang_specs.c | 2 +- 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 80c31d05c..4a8b73000 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -78,6 +78,7 @@ const char **cbm_string_dispatch_suffixes(CBMLanguage lang) { // Forward declarations static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang); static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source); +static const char *strip_and_validate_string_arg(CBMArena *a, char *text); // Lean 4: check if an apply node is inside a type annotation. // Strategy: walk up to the nearest declaration boundary; if the apply falls @@ -614,10 +615,99 @@ static char *extract_dart_callee(CBMArena *a, TSNode node, const char *source, c return NULL; } +// SCSS: an `@include foo;` is an include_statement whose callee is its +// `identifier` child (the mixin name). +static char *extract_scss_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "include_statement") != 0) { + return NULL; + } + TSNode id = cbm_find_child_by_kind(node, "identifier"); + return ts_node_is_null(id) ? NULL : cbm_node_text(a, id, source); +} + +// SQL: an `invocation` node's callee is nested object_reference > `name` field +// (the same shape as a create_function's name). +static char *extract_sql_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "invocation") != 0) { + return NULL; + } + TSNode oref = cbm_find_child_by_kind(node, "object_reference"); + if (ts_node_is_null(oref)) { + return NULL; + } + TSNode nm = ts_node_child_by_field_name(oref, TS_FIELD("name")); + return ts_node_is_null(nm) ? NULL : cbm_node_text(a, nm, source); +} + +// COBOL: a `CALL 'HELPER'` is a call_statement whose `x` field is a string +// literal naming the called program; the callee is that string sans quotes. +static char *extract_cobol_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "call_statement") != 0) { + return NULL; + } + TSNode x = ts_node_child_by_field_name(node, TS_FIELD("x")); + if (ts_node_is_null(x)) { + x = cbm_find_child_by_kind(node, "string"); + } + if (ts_node_is_null(x)) { + return NULL; + } + char *text = cbm_node_text(a, x, source); + return (char *)strip_and_validate_string_arg(a, text); +} + +// Elm: a `function_call_expr` has a `target` field; the callee identifier is +// target > value_expr > `name` field (value_qid) > lower_case_identifier. +static char *extract_elm_callee(CBMArena *a, TSNode node, const char *source, const char *nk) { + if (strcmp(nk, "function_call_expr") != 0) { + return NULL; + } + TSNode target = ts_node_child_by_field_name(node, TS_FIELD("target")); + if (ts_node_is_null(target)) { + return NULL; + } + TSNode ve = strcmp(ts_node_type(target), "value_expr") == 0 + ? target + : cbm_find_child_by_kind(target, "value_expr"); + if (ts_node_is_null(ve)) { + return NULL; + } + TSNode qid = ts_node_child_by_field_name(ve, TS_FIELD("name")); + if (ts_node_is_null(qid)) { + qid = cbm_find_child_by_kind(ve, "value_qid"); + } + if (ts_node_is_null(qid)) { + return NULL; + } + TSNode id = cbm_find_child_by_kind(qid, "lower_case_identifier"); + if (ts_node_is_null(id)) { + // module-qualified call: emit the whole qualified id text + return cbm_node_text(a, qid, source); + } + return cbm_node_text(a, id, source); +} + static char *extract_callee_lang_specific(CBMArena *a, TSNode node, const char *source, CBMLanguage lang) { const char *nk = ts_node_type(node); + if (lang == CBM_LANG_SCSS) { + char *c = extract_scss_callee(a, node, source, nk); + return c ? c : extract_scripting_callee(a, node, source, lang, nk); + } + if (lang == CBM_LANG_SQL) { + char *c = extract_sql_callee(a, node, source, nk); + return c ? c : extract_scripting_callee(a, node, source, lang, nk); + } + if (lang == CBM_LANG_COBOL) { + char *c = extract_cobol_callee(a, node, source, nk); + return c ? c : extract_scripting_callee(a, node, source, lang, nk); + } + if (lang == CBM_LANG_ELM) { + char *c = extract_elm_callee(a, node, source, nk); + return c ? c : extract_scripting_callee(a, node, source, lang, nk); + } + if (lang == CBM_LANG_CLOJURE || lang == CBM_LANG_COMMONLISP || lang == CBM_LANG_SCHEME || lang == CBM_LANG_FENNEL || lang == CBM_LANG_RACKET || lang == CBM_LANG_EMACSLISP) { return extract_lisp_callee(a, node, source, nk); diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index f5aa4b77d..44df77868 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -826,6 +826,57 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) { } } + /* Teal: the `local function foo()` form reduces to a function_statement + * whose name is carried on a `function_name` child rather than the `name` + * field (the field is only populated for the bare `function foo()` form). + * func_name_node() already handled the field case above; here we cover the + * function_name child so local functions also produce a Function def. */ + if (lang == CBM_LANG_TEAL && (strcmp(kind, "function_statement") == 0 || + strcmp(kind, "function_signature") == 0)) { + TSNode fn = cbm_find_child_by_kind(node, "function_name"); + if (!ts_node_is_null(fn)) { + return fn; + } + } + + /* SCSS: function_statement/mixin_statement have no `name` field; the def + * name is a plain `name` child node. */ + if (lang == CBM_LANG_SCSS && + (strcmp(kind, "function_statement") == 0 || strcmp(kind, "mixin_statement") == 0)) { + TSNode nm = cbm_find_child_by_kind(node, "name"); + if (!ts_node_is_null(nm)) { + return nm; + } + } + + /* SQL: create_function has no `name` field; the function name is nested as + * object_reference > `name` field (an identifier). */ + if (lang == CBM_LANG_SQL && strcmp(kind, "create_function") == 0) { + TSNode oref = cbm_find_child_by_kind(node, "object_reference"); + if (!ts_node_is_null(oref)) { + TSNode nm = ts_node_child_by_field_name(oref, TS_FIELD("name")); + if (!ts_node_is_null(nm)) { + return nm; + } + } + } + + /* Elm: value_declaration carries its name on the + * `functionDeclarationLeft` field's function_declaration_left child, + * whose first lower_case_identifier is the function name. */ + if (lang == CBM_LANG_ELM && strcmp(kind, "value_declaration") == 0) { + TSNode lhs = ts_node_child_by_field_name(node, TS_FIELD("functionDeclarationLeft")); + if (ts_node_is_null(lhs)) { + lhs = cbm_find_child_by_kind(node, "function_declaration_left"); + } + if (!ts_node_is_null(lhs)) { + TSNode nm = cbm_find_child_by_kind(lhs, "lower_case_identifier"); + if (!ts_node_is_null(nm)) { + return nm; + } + } + } + /* Pine Script: function_declaration_statement carries the name on the * `function` field (or `method` field for the method form), not `name`. */ if (lang == CBM_LANG_PINE && strcmp(kind, "function_declaration_statement") == 0) { diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 0c3192fea..524369585 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -636,7 +636,7 @@ static const char *css_import_types[] = {"import_statement", NULL}; // ==================== SCSS ==================== static const char *scss_func_types[] = {"mixin_statement", "function_statement", NULL}; static const char *scss_module_types[] = {"stylesheet", NULL}; -static const char *scss_call_types[] = {"call_expression", NULL}; +static const char *scss_call_types[] = {"call_expression", "include_statement", NULL}; static const char *scss_import_types[] = {"import_statement", "use_statement", "include_statement", NULL}; static const char *scss_branch_types[] = {"if_statement", NULL};