Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

294 lines
10 KiB
Go

package languages
import (
"path/filepath"
"regexp"
"gopkg.in/yaml.v3"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// Helm extraction gives Gortex a semantic view of a Helm chart: the
// chart itself (Chart.yaml) with its cross-chart dependencies, the
// named templates defined in `_helpers.tpl` / other `.tpl` files, and
// the include/template call edges between named templates (and from
// render manifests in templates/*.yaml into the helpers they pull in).
//
// Named templates are chart-wide: a `{{ define "x" }}` in any file and
// an `{{ include "x" }}` in any other file refer to the same template,
// so template nodes use a chart-canonical ID keyed only by the template
// name ("helm::template::"+NAME). include/template edges therefore
// resolve across files by ID without a second pass.
var (
// {{- define "NAME" -}} / {{ define "NAME" }} — tolerant of the
// {{- / -}} whitespace-trim markers and of single/double quotes.
helmDefineRe = regexp.MustCompile(`\{\{-?\s*define\s+["']([^"']+)["']\s*-?\}\}`)
// {{ end }} — closes a define (and other block actions). We pair
// define/end with a depth counter so include calls land on the
// innermost enclosing define.
helmEndRe = regexp.MustCompile(`\{\{-?\s*end\s*-?\}\}`)
// {{ include "NAME" . }} and {{ template "NAME" . }} — the two ways
// a template invokes another named template. The trailing context
// argument and any pipeline after it are ignored.
helmIncludeRe = regexp.MustCompile(`\{\{-?\s*(?:include|template)\s+["']([^"']+)["']`)
)
// HelmExtractor extracts Helm charts and Go-template named templates.
type HelmExtractor struct{}
func NewHelmExtractor() *HelmExtractor { return &HelmExtractor{} }
func (e *HelmExtractor) Language() string { return "helm" }
// Extensions claims Helm's named-template files (`.tpl`) and the
// `Chart.yaml` basename. `.tpl` is claimed ahead of the forest gotmpl
// grammar (registered earlier in RegisterAll) so the hand-written
// semantic layer wins. `.gotmpl` / `.tmpl` are deliberately left to the
// forest gotmpl grammar — Helm charts use `.tpl`, and those generic
// Go-template extensions are rarely Helm; YAML render templates with
// inline includes are handled by the yaml.go augmentation hook.
func (e *HelmExtractor) Extensions() []string {
return []string{".tpl", "Chart.yaml"}
}
func (e *HelmExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
if filepath.Base(filePath) == "Chart.yaml" {
return e.extractChart(filePath, src)
}
return e.extractTemplate(filePath, src)
}
// extractChart parses a Chart.yaml manifest: it emits a KindPackage
// chart node and an EdgeDependsOn to each declared dependency chart.
func (e *HelmExtractor) extractChart(filePath string, src []byte) (*parser.ExtractionResult, error) {
result := &parser.ExtractionResult{}
endLine := lineAt(src, len(src))
fileNode := &graph.Node{
ID: filePath, Kind: graph.KindFile, Name: filePath,
FilePath: filePath, StartLine: 1, EndLine: endLine,
Language: "helm",
}
result.Nodes = append(result.Nodes, fileNode)
var doc yaml.Node
if err := yaml.Unmarshal(src, &doc); err != nil {
// Malformed Chart.yaml: keep the file node so the file still
// indexes, but emit no chart semantics.
return result, nil
}
root := documentMapping(&doc)
if root == nil {
return result, nil
}
chartName := scalarOf(mappingGet(root, "name"))
if chartName == "" {
return result, nil
}
version := scalarOf(mappingGet(root, "version"))
chartID := helmChartID(chartName, version)
chartMeta := map[string]any{"helm_kind": "chart"}
if version != "" {
chartMeta["version"] = version
}
nameLine := 1
if nameKey := mappingGetKey(root, "name"); nameKey != nil && nameKey.Line > 0 {
nameLine = nameKey.Line
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: chartID, Kind: graph.KindPackage, Name: chartName,
FilePath: filePath, StartLine: nameLine, EndLine: endLine,
Language: "helm",
Meta: chartMeta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileNode.ID, To: chartID, Kind: graph.EdgeDefines,
FilePath: filePath, Line: nameLine,
})
// dependencies: [ {name, version, repository}, ... ] — emit a
// cross-chart EdgeDependsOn to each dependency's chart node. The
// target ID is keyed by name only (no version) so it resolves to a
// dependency chart regardless of the exact version pinned here.
for _, dep := range sequenceItems(mappingGet(root, "dependencies")) {
depName := scalarOf(mappingGet(dep, "name"))
if depName == "" {
continue
}
depLine := dep.Line
if depLine <= 0 {
depLine = nameLine
}
depMeta := map[string]any{"helm_kind": "dependency"}
if depVer := scalarOf(mappingGet(dep, "version")); depVer != "" {
depMeta["version"] = depVer
}
if repo := scalarOf(mappingGet(dep, "repository")); repo != "" {
depMeta["repository"] = repo
}
result.Edges = append(result.Edges, &graph.Edge{
From: chartID, To: "helm::chart::" + depName,
Kind: graph.EdgeDependsOn,
FilePath: filePath, Line: depLine,
Meta: depMeta,
})
}
return result, nil
}
// extractTemplate scans a `.tpl` / `.gotmpl` file for `define` blocks
// (KindFunction nodes) and the include/template calls between them.
func (e *HelmExtractor) extractTemplate(filePath string, src []byte) (*parser.ExtractionResult, error) {
result := &parser.ExtractionResult{}
endLine := lineAt(src, len(src))
fileNode := &graph.Node{
ID: filePath, Kind: graph.KindFile, Name: filePath,
FilePath: filePath, StartLine: 1, EndLine: endLine,
Language: "helm",
}
result.Nodes = append(result.Nodes, fileNode)
seen := make(map[string]bool)
for _, m := range helmDefineRe.FindAllSubmatchIndex(src, -1) {
name := string(src[m[2]:m[3]])
if name == "" {
continue
}
id := "helm::template::" + name
line := lineAt(src, m[0])
if !seen[id] {
seen[id] = true
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindFunction, Name: name,
FilePath: filePath, StartLine: line, EndLine: line,
Language: "helm",
Meta: map[string]any{"helm_kind": "named_template"},
})
}
result.Edges = append(result.Edges, &graph.Edge{
From: fileNode.ID, To: id, Kind: graph.EdgeDefines,
FilePath: filePath, Line: line,
})
}
helmScanCalls(filePath, fileNode.ID, src, result)
return result, nil
}
// helmScanCalls walks the template directives in document order and
// emits an EdgeCalls for every include/template invocation. The caller
// is the innermost enclosing `define` block (resolved by pairing
// define/end with a depth counter); when a call sits outside any define
// the file node is the caller.
func helmScanCalls(filePath, fileID string, src []byte, result *parser.ExtractionResult) {
// Collect the boundary directives (define-open / end) and the call
// sites, all keyed by byte offset, then merge them in offset order.
type marker struct {
pos int
kind int // 0 = define-open, 1 = end, 2 = call
name string // template name for define-open and call
}
var markers []marker
for _, m := range helmDefineRe.FindAllSubmatchIndex(src, -1) {
markers = append(markers, marker{pos: m[0], kind: 0, name: string(src[m[2]:m[3]])})
}
for _, m := range helmEndRe.FindAllIndex(src, -1) {
markers = append(markers, marker{pos: m[0], kind: 1})
}
for _, m := range helmIncludeRe.FindAllSubmatchIndex(src, -1) {
markers = append(markers, marker{pos: m[0], kind: 2, name: string(src[m[2]:m[3]])})
}
// Insertion sort by byte offset — the directive spans are disjoint,
// so offsets never collide and ordering is total.
for i := 1; i < len(markers); i++ {
for j := i; j > 0 && markers[j-1].pos > markers[j].pos; j-- {
markers[j-1], markers[j] = markers[j], markers[j-1]
}
}
// defineStack holds the IDs of the open define blocks. Note: `end`
// also closes if/range/with blocks, so the stack can underflow
// relative to defines; we only push for define and pop on any end,
// which keeps the innermost-define attribution correct for the
// common, well-formed helper layouts Helm charts use.
var defineStack []string
for _, mk := range markers {
switch mk.kind {
case 0: // define-open
defineStack = append(defineStack, "helm::template::"+mk.name)
case 1: // end
if n := len(defineStack); n > 0 {
defineStack = defineStack[:n-1]
}
case 2: // include/template call
if mk.name == "" {
continue
}
from := fileID
if n := len(defineStack); n > 0 {
from = defineStack[n-1]
}
result.Edges = append(result.Edges, &graph.Edge{
From: from, To: "helm::template::" + mk.name,
Kind: graph.EdgeCalls,
FilePath: filePath, Line: lineAt(src, mk.pos),
})
}
}
}
// helmTemplateCallsFromYAML scans a render manifest (templates/*.yaml)
// for include/template directives and emits an EdgeCalls from the file
// to each invoked named template. It is additive: the YAML extractor
// calls it before its normal dispatch so the file still gets its
// config-key / resource nodes. Render manifests are never inside a
// `define`, so the caller is always the file node.
func helmTemplateCallsFromYAML(filePath, fileID string, src []byte, result *parser.ExtractionResult) {
for _, m := range helmIncludeRe.FindAllSubmatchIndex(src, -1) {
name := string(src[m[2]:m[3]])
if name == "" {
continue
}
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: "helm::template::" + name,
Kind: graph.EdgeCalls,
FilePath: filePath, Line: lineAt(src, m[0]),
})
}
}
// helmChartID is the canonical ID for a Helm chart node. The version,
// when present, is appended so charts with the same name but different
// versions stay distinct; dependency edges target the version-less form
// ("helm::chart::"+name) so they resolve regardless of the pinned range.
func helmChartID(name, version string) string {
if version != "" {
return "helm::chart::" + name + "@" + version
}
return "helm::chart::" + name
}
// mappingGetKey returns the *key* node for the given key inside a
// MappingNode (mappingGet returns the value node). Used to anchor the
// chart node to the line of its `name:` key.
func mappingGetKey(parent *yaml.Node, key string) *yaml.Node {
if parent == nil || parent.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(parent.Content); i += 2 {
k := parent.Content[i]
if k != nil && k.Value == key {
return k
}
}
return nil
}
var _ parser.Extractor = (*HelmExtractor)(nil)