From c8187e786e29a2312c5d9b84f88bd4d187dc0037 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 1 Nov 2025 21:14:27 +0100 Subject: [PATCH 1/3] feat(values): lint unused values in values.yaml https://github.com/mrjosh/helm-ls/issues/176 --- internal/handler/yaml_handler/diagnostics.go | 41 +++++---- internal/helm_lint/values_lint.go | 87 ++++++++++++++++++++ internal/util/yaml_goccy.go | 13 +++ 3 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 internal/helm_lint/values_lint.go diff --git a/internal/handler/yaml_handler/diagnostics.go b/internal/handler/yaml_handler/diagnostics.go index ba9e774c..8dd9f833 100644 --- a/internal/handler/yaml_handler/diagnostics.go +++ b/internal/handler/yaml_handler/diagnostics.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" + helmlint "github.com/mrjosh/helm-ls/internal/helm_lint" "go.lsp.dev/protocol" "go.lsp.dev/uri" ) @@ -15,16 +16,22 @@ var lineNumberRegex = regexp.MustCompile("line ([0-9]+): (.*)") // GetDiagnostics implements handler.LangHandler. func (h *YamlHandler) GetDiagnostics(uri uri.URI) []protocol.PublishDiagnosticsParams { doc, ok := h.documents.GetYamlDoc(uri) + chart, err := h.chartStore.GetChartForDoc(uri) + diagnostics := []protocol.Diagnostic{} if !ok { return nil } + if err == nil { + diagnostics = append(diagnostics, helmlint.LintUnusedValues(chart, doc, h.documents.GetAllTemplateDocs())...) + } + if doc.ParseErr == nil { logger.Debug("YamlHandler: No parse error") return []protocol.PublishDiagnosticsParams{{ URI: uri, - Diagnostics: []protocol.Diagnostic{}, + Diagnostics: diagnostics, }} } @@ -55,25 +62,23 @@ func (h *YamlHandler) GetDiagnostics(uri uri.URI) []protocol.PublishDiagnosticsP return []protocol.PublishDiagnosticsParams{ { URI: uri, - Diagnostics: []protocol.Diagnostic{ - { - Range: protocol.Range{ - Start: protocol.Position{ - Line: lineUint, - Character: 0, - }, - End: protocol.Position{ - Line: lineUint + 1, - Character: 0, - }, + Diagnostics: append(diagnostics, protocol.Diagnostic{ + Range: protocol.Range{ + Start: protocol.Position{ + Line: lineUint, + Character: 0, + }, + End: protocol.Position{ + Line: lineUint + 1, + Character: 0, }, - Source: "Helm-ls YamlHandler", - Message: matches[2], - Tags: []protocol.DiagnosticTag{}, - RelatedInformation: []protocol.DiagnosticRelatedInformation{}, - Data: nil, }, - }, + Source: "Helm-ls YamlHandler", + Message: matches[2], + Tags: []protocol.DiagnosticTag{}, + RelatedInformation: []protocol.DiagnosticRelatedInformation{}, + Data: nil, + }), }, } } diff --git a/internal/helm_lint/values_lint.go b/internal/helm_lint/values_lint.go new file mode 100644 index 00000000..3f64da92 --- /dev/null +++ b/internal/helm_lint/values_lint.go @@ -0,0 +1,87 @@ +package helmlint + +import ( + "fmt" + + "github.com/goccy/go-yaml/ast" + "github.com/mrjosh/helm-ls/internal/charts" + "github.com/mrjosh/helm-ls/internal/lsp/document" + "github.com/mrjosh/helm-ls/internal/lsp/symboltable" + "github.com/mrjosh/helm-ls/internal/util" + lsp "go.lsp.dev/protocol" +) + +func LintUnusedValues(chart *charts.Chart, doc *document.YamlDocument, templateDocs []*document.TemplateDocument) []lsp.Diagnostic { + if len(templateDocs) == 0 { + // TODO: delay linting until the template docs are loaded + return []lsp.Diagnostic{} + } + + result := []lsp.Diagnostic{} + for _, node := range FindNodes(doc.GoccyYamlNode) { + + templateContext := symboltable.TemplateContextFromYAMLPath(node.GetPath()) + found := false + + for _, templateDoc := range templateDocs { + // TODO(dependecy-charts): template context would need to be adjusted for dependency charts + // see https://github.com/mrjosh/helm-ls/issues/152 + referenceRanges := templateDoc.SymbolTable.GetTemplateContextRanges(append([]string{"Values"}, templateContext...)) + + logger.Println(fmt.Sprintf("LintUnusedValues: checking template context %v in template doc %s", templateContext, templateDoc.GetPath())) + + if len(referenceRanges) > 0 { + found = true + break + } + } + + if found { + continue + } + + fmt.Println(node.String()) + result = append(result, lsp.Diagnostic{ + Range: util.TokenToRange(node.GetToken()), + Severity: 0, + Code: nil, + CodeDescription: &lsp.CodeDescription{}, + Source: "", + Message: fmt.Sprintf("Unused value: %s of type %s", node.GetPath(), node.Type()), + Tags: []lsp.DiagnosticTag{ + lsp.DiagnosticTagUnnecessary, + }, + RelatedInformation: []lsp.DiagnosticRelatedInformation{}, + Data: nil, + }) + } + + return result +} + +func FindNodes(node ast.Node) []ast.Node { + visitor := &LeafMappingsFinderVisitor{} + ast.Walk(visitor, node) + return visitor.result +} + +// PositionFinderVisitor is a visitor that collects positions. +type LeafMappingsFinderVisitor struct { + result []ast.Node +} + +func (v *LeafMappingsFinderVisitor) Visit(node ast.Node) ast.Visitor { + if IsLeafMapping(node) { + v.result = append(v.result, node) + } + return v +} + +func IsLeafMapping(node ast.Node) bool { + switch node.Type() { + case ast.MappingKeyType, ast.MappingValueType, ast.MappingType, ast.SequenceType: + return false + default: + return true + } +} diff --git a/internal/util/yaml_goccy.go b/internal/util/yaml_goccy.go index 0ef7cf6b..fe5551c2 100644 --- a/internal/util/yaml_goccy.go +++ b/internal/util/yaml_goccy.go @@ -5,7 +5,9 @@ import ( "github.com/goccy/go-yaml" "github.com/goccy/go-yaml/ast" + "github.com/goccy/go-yaml/token" "go.lsp.dev/protocol" + lsp "go.lsp.dev/protocol" ) func GetNodeForPosition(node ast.Node, position protocol.Position) ast.Node { @@ -83,3 +85,14 @@ func ReadYamlToGoccyNode(data []byte) (node ast.Node, err error) { err = yaml.Unmarshal(normalizedData, &node) return node, err } + +func TokenToRange(token *token.Token) lsp.Range { + if token == nil { + return lsp.Range{} + } + + return lsp.Range{ + Start: lsp.Position{Line: uint32(token.Position.Line - 1), Character: uint32(token.Position.Column - 1)}, + End: lsp.Position{Line: uint32(token.Position.Line - 1), Character: uint32(token.Position.Column+len(token.Value)) + 1}, + } +} From 780fcd0c3633267ece074e12dfa1f0afad570ae9 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 1 Nov 2025 21:25:21 +0100 Subject: [PATCH 2/3] fix: change Severity --- internal/helm_lint/values_lint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/helm_lint/values_lint.go b/internal/helm_lint/values_lint.go index 3f64da92..f4a52c25 100644 --- a/internal/helm_lint/values_lint.go +++ b/internal/helm_lint/values_lint.go @@ -43,10 +43,10 @@ func LintUnusedValues(chart *charts.Chart, doc *document.YamlDocument, templateD fmt.Println(node.String()) result = append(result, lsp.Diagnostic{ Range: util.TokenToRange(node.GetToken()), - Severity: 0, + Severity: lsp.DiagnosticSeverityHint, Code: nil, CodeDescription: &lsp.CodeDescription{}, - Source: "", + Source: "helm-ls unused values", Message: fmt.Sprintf("Unused value: %s of type %s", node.GetPath(), node.Type()), Tags: []lsp.DiagnosticTag{ lsp.DiagnosticTagUnnecessary, From 90891877a64f5705cff3133eba98ea476ddd6a5d Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 29 Nov 2025 18:34:38 +0100 Subject: [PATCH 3/3] fix: remove Println --- internal/helm_lint/values_lint.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/helm_lint/values_lint.go b/internal/helm_lint/values_lint.go index f4a52c25..a1ae755a 100644 --- a/internal/helm_lint/values_lint.go +++ b/internal/helm_lint/values_lint.go @@ -40,7 +40,6 @@ func LintUnusedValues(chart *charts.Chart, doc *document.YamlDocument, templateD continue } - fmt.Println(node.String()) result = append(result, lsp.Diagnostic{ Range: util.TokenToRange(node.GetToken()), Severity: lsp.DiagnosticSeverityHint,