From 3070590a31e6776f733f446fbbaca143056f0e5f Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:03:38 -0700 Subject: [PATCH] feat: add KDL file validation support (#463) KDL (KDL Document Language, https://kdl.dev) is a node-based config format used by Zellij and other tools. Add a syntax-only KDL validator wired into the existing FileType registry. - New KdlValidator in pkg/validator/kdl.go uses sblinch/kdl-go's Parse function. Errors that the parser annotates with "at line N, column M" surface through ValidationError so editors see the right caret position; other errors fall through unchanged. - New KdlFileType in pkg/filetype with the .kdl extension. Added to fileTypeRegistry and the FileTypes slice so it participates in Linguist known-file resolution and the standard file walk. - Two valid + two invalid table-driven test cases (top-level nodes, child blocks, unterminated strings, unclosed children). - README updated: 16 -> 17 file formats and a new KDL row in the Supported File Types matrix. Schema column is "no" - KDL has no schema system, only syntax validation. Note: sblinch/kdl-go targets KDLv1, which is what most existing .kdl files use today (Zellij, etc). KDLv2 syntax that's incompatible with v1 will fail this validator. We can swap to a v2-aware parser when one stabilises in the Go ecosystem. --- README.md | 3 ++- go.mod | 4 ++-- go.sum | 14 ++----------- pkg/filetype/file_type.go | 11 ++++++++++ pkg/validator/kdl.go | 37 +++++++++++++++++++++++++++++++++ pkg/validator/validator_test.go | 4 ++++ 6 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 pkg/validator/kdl.go diff --git a/README.md b/README.md index 1601a076..802596c9 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Config File Validator is a cross-platform CLI tool that validates configuration files in your project. Catch syntax errors, schema violations, and misconfigurations across all your config files — with one tool. - **Single binary, zero dependencies** — no runtimes, no package managers, just one executable -- **16 file formats** — JSON, JSONC, YAML, TOML, XML, HCL, INI, HOCON, ENV, CSV, Properties, EDITORCONFIG, Justfile, PList, SARIF, and TOON +- **17 file formats** — JSON, JSONC, YAML, TOML, XML, HCL, INI, HOCON, ENV, CSV, Properties, EDITORCONFIG, Justfile, KDL, PList, SARIF, and TOON - **Syntax + schema validation** — validates structure with [JSON Schema](https://json-schema.org/) and XSD, with automatic [SchemaStore](https://www.schemastore.org/) integration - **CI/CD ready** — JSON, JUnit, and SARIF output for GitHub Actions, GitLab CI, Jenkins, and more - **Configurable** — project-level `.cfv.toml` config files, glob patterns, schema mappings, and environment variables @@ -77,6 +77,7 @@ Config File Validator is a cross-platform CLI tool that validates configuration | Justfile | ✅ | ❌ | | JSON | ✅ | ✅ | | JSONC | ✅ | ✅ | +| KDL | ✅ | ❌ | | Properties | ✅ | ❌ | | SARIF | ✅ | ✅ | | TOML | ✅ | ✅ | diff --git a/go.mod b/go.mod index e86084e7..d1e18242 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,8 @@ require ( howett.net/plist v1.0.1 ) +require github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 + require ( github.com/Boeing/go-just v0.0.0 github.com/agext/levenshtein v1.2.1 // indirect @@ -35,13 +37,11 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/lestrrat-go/helium v0.0.1 github.com/lestrrat-go/pdebug v0.0.0-20210111095411-35b07dbf089b // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/zclconf/go-cty v1.13.0 // indirect diff --git a/go.sum b/go.sum index bdd01f7a..0465252b 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E= +github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -104,18 +106,6 @@ golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/filetype/file_type.go b/pkg/filetype/file_type.go index e5a14ece..a09ba609 100644 --- a/pkg/filetype/file_type.go +++ b/pkg/filetype/file_type.go @@ -158,6 +158,15 @@ var JustfileFileType = FileType{ Validator: validator.JustfileValidator{}, } +// Instance of the FileType object to +// represent a KDL (KDL Document Language) file. +// See https://kdl.dev/ for the spec. +var KdlFileType = FileType{ + Name: "kdl", + Extensions: arrToMap("kdl"), + Validator: validator.KdlValidator{}, +} + // extraKnownFiles contains manual entries not covered by Linguist. var extraKnownFiles = map[string][]string{ "ini": { @@ -187,6 +196,7 @@ var fileTypeRegistry = map[string]*FileType{ "toon": &ToonFileType, "sarif": &SarifFileType, "justfile": &JustfileFileType, + "kdl": &KdlFileType, } // excludeKnownFiles lists Linguist entries to skip because we have @@ -258,6 +268,7 @@ func init() { SarifFileType, JSONCFileType, JustfileFileType, + KdlFileType, } } diff --git a/pkg/validator/kdl.go b/pkg/validator/kdl.go new file mode 100644 index 00000000..e57f7185 --- /dev/null +++ b/pkg/validator/kdl.go @@ -0,0 +1,37 @@ +package validator + +import ( + "bytes" + "regexp" + "strconv" + + kdl "github.com/sblinch/kdl-go" +) + +// kdlLineRe matches the "at line N, column M" suffix the upstream parser +// appends to scan/parse errors so we can surface the position via +// ValidationError instead of leaving it buried in the message. +var kdlLineRe = regexp.MustCompile(`at line (\d+), column (\d+)`) + +// KdlValidator validates KDL Document Language files via the sblinch/kdl-go +// parser. It implements ValidateSyntax only — KDL has no schema system, so +// it does not implement SchemaValidator. +// +// See https://kdl.dev/ for the KDL spec. +type KdlValidator struct{} + +var _ Validator = KdlValidator{} + +func (KdlValidator) ValidateSyntax(b []byte) (bool, error) { + if _, err := kdl.Parse(bytes.NewReader(b)); err != nil { + if m := kdlLineRe.FindStringSubmatch(err.Error()); m != nil { + line, lineErr := strconv.Atoi(m[1]) + col, colErr := strconv.Atoi(m[2]) + if lineErr == nil && colErr == nil { + return false, &ValidationError{Err: err, Line: line, Column: col} + } + } + return false, err + } + return true, nil +} diff --git a/pkg/validator/validator_test.go b/pkg/validator/validator_test.go index 411cdeef..09854323 100644 --- a/pkg/validator/validator_test.go +++ b/pkg/validator/validator_test.go @@ -162,6 +162,10 @@ var testData = []struct { {"validJsoncTrailingComma", []byte(`{"a": 1, "b": 2,}`), true, JSONCValidator{}}, {"invalidJsonc", []byte(`{"bad": }`), false, JSONCValidator{}}, {"validJsoncNoComments", []byte(`{"key": "value"}`), true, JSONCValidator{}}, + {"validKdl", []byte("name \"Bob\"\nage 76\nactive true\n"), true, KdlValidator{}}, + {"validKdlChildren", []byte("package {\n name \"foo\"\n version \"1.0\"\n}\n"), true, KdlValidator{}}, + {"invalidKdlUnterminatedString", []byte("name \"Bob\n"), false, KdlValidator{}}, + {"invalidKdlUnclosedChildren", []byte("package {\n name \"foo\"\n"), false, KdlValidator{}}, } func Test_ValidationInput(t *testing.T) {