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

138 lines
4.7 KiB
Go

package languages
import (
"bytes"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
"github.com/zzet/gortex/internal/parser/tsitter/yaml"
)
// YAMLExtractor extracts YAML files into graph nodes and edges.
// It focuses on top-level keys as KindVariable.
type YAMLExtractor struct {
lang *sitter.Language
}
func NewYAMLExtractor() *YAMLExtractor {
return &YAMLExtractor{lang: yaml.GetLanguage()}
}
func (e *YAMLExtractor) Language() string { return "yaml" }
func (e *YAMLExtractor) Extensions() []string {
// `.yaml` / `.yml` cover the bulk of YAML files (including
// `kustomization.yaml`). `Kustomization` is the bare-basename
// form Kustomize accepts when no extension is desired — it
// must be registered as a basename so the registry routes it
// to the YAML extractor (which then dispatches into the
// kustomize path inside Extract).
return []string{".yaml", ".yml", "Kustomization"}
}
func (e *YAMLExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
tree, err := parser.ParseFile(src, e.lang)
if err != nil {
return nil, err
}
defer tree.Close()
root := tree.RootNode()
result := &parser.ExtractionResult{}
fileNode := &graph.Node{
ID: filePath, Kind: graph.KindFile, Name: filePath,
FilePath: filePath, StartLine: 1, EndLine: int(root.EndPoint().Row) + 1,
Language: "yaml",
}
result.Nodes = append(result.Nodes, fileNode)
// Helm render manifests (templates/*.yaml) embed Go-template
// include/template directives. Scan them up front and emit EdgeCalls
// into the chart's named templates — this augments, it does not
// replace, the normal dispatch below (the file still gets its
// resource / config-key nodes). Conservative: only when an
// include/template directive is actually present.
if bytes.Contains(src, []byte("{{")) &&
(bytes.Contains(src, []byte("include")) || bytes.Contains(src, []byte("template"))) {
helmTemplateCallsFromYAML(filePath, fileNode.ID, src, result)
}
// Specialised YAML dispatch. Order matters:
// 1. Kustomize files have a fixed basename — short-circuit.
// 2. K8s manifests are detected by content (apiVersion+kind).
// 3. dbt schema / properties files are detected by content
// fingerprint (models/sources/seeds/snapshots + columns).
// 4. Otherwise fall through to the generic top-level-keys
// walker so plain config YAMLs still index.
if isKustomizationFile(filePath) {
extractKustomizeYAML(filePath, fileNode.ID, src, result)
return result, nil
}
if extractKubernetesYAML(filePath, fileNode.ID, src, result) {
return result, nil
}
if extractDbtSchemaYAML(filePath, fileNode.ID, src, result) {
return result, nil
}
if extractSymfonyServicesYAML(filePath, fileNode.ID, src, result) {
return result, nil
}
if extractAnsibleYAML(filePath, fileNode.ID, src, result) {
return result, nil
}
// Walk only top-level block_mapping_pair nodes.
e.extractTopLevelKeys(root, src, filePath, fileNode.ID, result)
return result, nil
}
func (e *YAMLExtractor) extractTopLevelKeys(root *sitter.Node, src []byte, filePath, fileID string, result *parser.ExtractionResult) {
// YAML tree-sitter grammar: stream -> document -> block_node -> block_mapping -> block_mapping_pair
// We walk looking for block_mapping_pair nodes at the top-level mapping only.
seen := make(map[string]bool)
e.findTopLevelPairs(root, src, filePath, fileID, result, seen, 0)
}
func (e *YAMLExtractor) findTopLevelPairs(node *sitter.Node, src []byte, filePath, fileID string, result *parser.ExtractionResult, seen map[string]bool, depth int) {
if node == nil {
return
}
nodeType := node.Type()
// If we hit a block_mapping_pair at the top-level mapping, extract the key.
if nodeType == "block_mapping_pair" && depth <= 5 {
// The first child is typically the key.
keyNode := node.ChildByFieldName("key")
if keyNode == nil && node.ChildCount() > 0 {
keyNode = node.Child(0)
}
if keyNode != nil {
keyName := keyNode.Content(src)
if keyName != "" && !seen[keyName] {
seen[keyName] = true
id := filePath + "::" + keyName
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindVariable, Name: keyName,
FilePath: filePath, StartLine: int(node.StartPoint().Row) + 1, EndLine: int(node.EndPoint().Row) + 1,
Language: "yaml",
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines,
FilePath: filePath, Line: int(node.StartPoint().Row) + 1,
})
}
}
return // Don't recurse into nested mappings.
}
for i, _nc := 0, int(node.ChildCount()); i < _nc; i++ {
child := node.Child(i)
if child != nil {
e.findTopLevelPairs(child, src, filePath, fileID, result, seen, depth+1)
}
}
}