From a8fc901129ee180476b3365d9e6a48ca53f7eb6a Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 1 Jun 2026 18:41:49 +0200 Subject: [PATCH] feat(helm): extract Helm/Go-template named templates and include calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helm chart helpers (.tpl) and Go templates produced no semantic graph — just a flat key flood. Wire the existing gotemplate grammar for real structure: - .tpl files map to the gotemplate language (Helm _helpers.tpl). - {{ define "chart.fullname" }} becomes a Function node named after the template (the name is a string literal, not an identifier, so it's resolved in a dedicated walker branch). - {{ include "x" . }} and {{ template "x" . }} resolve to the referenced template name instead of the bare `include` builtin, so they form CALLS edges to the define'd Function. template_action is added to the call set. Lets you trace which charts include which named templates. Tests cover define -> Function and include/template -> CALLS, plus the .tpl mapping. Refs #338 --- internal/cbm/extract_calls.c | 52 ++++++++++++++++++++++++++++++++++++ internal/cbm/extract_defs.c | 39 +++++++++++++++++++++++++++ internal/cbm/lang_specs.c | 3 ++- src/discover/language.c | 1 + tests/test_extraction.c | 23 ++++++++++++++++ tests/test_language.c | 6 +++++ 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index fa78f61d5..8a7247809 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -60,6 +60,7 @@ static const char *strip_quotes(CBMArena *a, const char *text) { static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec); static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, CBMLanguage lang); static void extract_jsx_refs(CBMExtractCtx *ctx, TSNode node); +static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source); // Lean 4: check if an apply node is inside a type annotation. // Strategy: walk up to the nearest declaration boundary; if the apply falls @@ -355,6 +356,15 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C } } + // Helm / Go templates: resolve `include "x"` / `template "x"` to the + // referenced named template so it links to the define'd Function (#338). + if (lang == CBM_LANG_GOTEMPLATE) { + char *g = gotemplate_callee(a, node, source); + if (g) { + return g; + } + } + // Try common field-based resolution first char *name = extract_callee_from_fields(a, node, source); if (name) { @@ -413,6 +423,48 @@ static const char *extract_first_string_arg(CBMExtractCtx *ctx, TSNode args) { return NULL; } +// Return the (dequoted) first string-literal child of a node, or NULL. +static char *gotemplate_string_child(CBMArena *a, TSNode parent, const char *source) { + TSNode s = cbm_find_child_by_kind(parent, "interpreted_string_literal"); + if (ts_node_is_null(s)) { + return NULL; + } + char *text = cbm_node_text(a, s, source); + const char *v = strip_and_validate_string_arg(a, text); + return (char *)v; +} + +// Resolve a Go-template / Helm call to the referenced named template: +// {{ template "x" . }} -> template_action, name is a string child +// {{ include "x" . }} -> function_call(include), name is first string arg +// Returns NULL for any other node so generic resolution names the function. +static char *gotemplate_callee(CBMArena *a, TSNode node, const char *source) { + const char *k = ts_node_type(node); + if (strcmp(k, "template_action") == 0) { + return gotemplate_string_child(a, node, source); + } + if (strcmp(k, "function_call") == 0) { + TSNode fn = cbm_find_child_by_kind(node, "identifier"); + if (ts_node_is_null(fn)) { + return NULL; + } + char *fname = cbm_node_text(a, fn, source); + if (!fname || (strcmp(fname, "include") != 0 && strcmp(fname, "template") != 0 && + strcmp(fname, "tpl") != 0)) { + return NULL; + } + TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments")); + if (ts_node_is_null(args)) { + args = cbm_find_child_by_kind(node, "argument_list"); + } + if (ts_node_is_null(args)) { + return NULL; + } + return gotemplate_string_child(a, args, source); + } + return NULL; +} + // Walk AST for call nodes (iterative) static void walk_calls(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { TSNodeStack stack; diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index ab71eb622..e38de8718 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -3674,6 +3674,40 @@ static void extract_cfml_function_tag(CBMExtractCtx *ctx, TSNode node) { cbm_defs_push(&ctx->result->defs, a, def); } +// Helm / Go template named-template definition: {{ define "chart.fullname" }} ... +// {{ end }} (#338). The name is a string-literal child of define_action. Emit a +// Function node so `include`/`template` references resolve to it via CALLS. +static void extract_gotemplate_define(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + TSNode s = cbm_find_child_by_kind(node, "interpreted_string_literal"); + if (ts_node_is_null(s)) { + return; + } + char *raw = cbm_node_text(a, s, ctx->source); + if (!raw) { + return; + } + size_t len = strlen(raw); + if (len >= 2 && (raw[0] == '"' || raw[0] == '`')) { + raw = cbm_arena_strndup(a, raw + 1, len - 2); // strip surrounding quotes + } + if (!raw || !raw[0]) { + return; + } + + CBMDefinition def; + memset(&def, 0, sizeof(def)); + def.name = raw; + def.qualified_name = cbm_fqn_compute(a, ctx->project, ctx->rel_path, raw); + def.label = "Function"; + def.file_path = ctx->rel_path; + def.start_line = ts_node_start_point(node).row + TS_LINE_OFFSET; + def.end_line = ts_node_end_point(node).row + TS_LINE_OFFSET; + def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET); + def.is_exported = true; + cbm_defs_push(&ctx->result->defs, a, def); +} + // Languages that use the C preprocessor and therefore have #define macros. static bool is_c_preprocessor_lang(CBMLanguage lang) { return lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || @@ -3743,6 +3777,11 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec, // fall through: descend into the body for nested tags / calls } + if (ctx->language == CBM_LANG_GOTEMPLATE && strcmp(kind, "define_action") == 0) { + extract_gotemplate_define(ctx, node); + // fall through: descend into the body for nested defines + } + if (cbm_kind_in_set(node, spec->function_node_types)) { if (!is_template_class_node(node, ctx->language)) { extract_func_def(ctx, node, spec); diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index fadc059c7..2043d65d7 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -1251,7 +1251,8 @@ static const char *just_import_types[] = {"import", NULL}; static const char *just_branch_types[] = {"if_expression", NULL}; static const char *just_assign_types[] = {"assignment", NULL}; static const char *just_module_types[] = {"source_file", NULL}; -static const char *gotemplate_call_types[] = {"function_call", "method_call", NULL}; +static const char *gotemplate_call_types[] = {"function_call", "method_call", "template_action", + NULL}; static const char *gotemplate_module_types[] = {"template", NULL}; static const char *templ_func_types[] = {"function_declaration", "method_declaration", "method_elem", NULL}; diff --git a/src/discover/language.c b/src/discover/language.c index 7df8f218f..56f9f8521 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -372,6 +372,7 @@ static const ext_entry_t EXT_TABLE[] = { /* Go Template */ {".gotmpl", CBM_LANG_GOTEMPLATE}, + {".tpl", CBM_LANG_GOTEMPLATE}, /* Helm _helpers.tpl named-template definitions */ /* Hare */ {".ha", CBM_LANG_HARE}, diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 0250edbdc..797c5929a 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -277,6 +277,28 @@ TEST(extract_cfml_tag_issue38) { PASS(); } +/* --- Helm / Go template: named templates + include calls (#338) --- */ +TEST(extract_helm_templates_issue338) { + CBMFileResult *r = extract("{{- define \"chart.fullname\" -}}\n" + "{{- .Release.Name -}}\n" + "{{- end -}}\n" + "\n" + "{{- define \"chart.labels\" -}}\n" + "app: {{ include \"chart.fullname\" . }}\n" + "chart: {{ template \"chart.fullname\" . }}\n" + "{{- end -}}\n", + CBM_LANG_GOTEMPLATE, "chart", "templates/_helpers.tpl"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + /* define -> Function nodes */ + ASSERT(has_def(r, "Function", "chart.fullname")); + ASSERT(has_def(r, "Function", "chart.labels")); + /* include / template -> CALLS to the named template (not to "include") */ + ASSERT(has_call(r, "chart.fullname")); + cbm_free_result(r); + PASS(); +} + /* --- Java --- */ TEST(java_class) { CBMFileResult *r = extract( @@ -2554,6 +2576,7 @@ SUITE(extraction) { RUN_TEST(extract_qml_issue42); RUN_TEST(extract_cfscript_issue38); RUN_TEST(extract_cfml_tag_issue38); + RUN_TEST(extract_helm_templates_issue338); /* OOP */ RUN_TEST(java_class); diff --git a/tests/test_language.c b/tests/test_language.c index 23eb25ee6..f51048f10 100644 --- a/tests/test_language.c +++ b/tests/test_language.c @@ -759,6 +759,11 @@ TEST(lang_ext_cfml) { PASS(); } +TEST(lang_ext_helm_tpl) { + ASSERT_EQ(cbm_language_for_extension(".tpl"), CBM_LANG_GOTEMPLATE); + PASS(); +} + TEST(lang_ext_janet) { ASSERT_EQ(cbm_language_for_extension(".janet"), CBM_LANG_JANET); PASS(); @@ -1229,6 +1234,7 @@ SUITE(language) { RUN_TEST(lang_ext_luau); RUN_TEST(lang_ext_qml); RUN_TEST(lang_ext_cfml); + RUN_TEST(lang_ext_helm_tpl); RUN_TEST(lang_ext_janet); RUN_TEST(lang_ext_sway); RUN_TEST(lang_ext_nasm);