From 424e5566d4a760ca9e530dfdf318c0c614e20192 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 5 Sep 2026 09:33:23 +0100 Subject: [PATCH] Accept workspace-inherited [package] fields in Cargo.toml Cargo workspace inheritance (RFC 2906, Rust 1.64+) lets [package] version, license, and license-file be written as {workspace = true} instead of a string. Decoding those into a string field made toml.Decode error on the whole file, so dependencies were lost too. Add an inheritableString type whose UnmarshalTOML accepts either form, yielding the zero value for the table form since the workspace root is not visible from a single-file parse. Name is not inheritable and stays a plain string. --- internal/cargo/cargo.go | 27 +++++++++++++----- internal/cargo/cargo_test.go | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/internal/cargo/cargo.go b/internal/cargo/cargo.go index 89d3e00..f7dc885 100644 --- a/internal/cargo/cargo.go +++ b/internal/cargo/cargo.go @@ -19,13 +19,26 @@ func init() { // cargoTomlParser parses Cargo.toml files. type cargoTomlParser struct{} +// inheritableString decodes a Cargo [package] field that may be a literal +// string or the workspace-inheritance table form {workspace = true}. The +// inherited value is not resolvable from a single file, so the table form +// yields the zero value instead of failing the whole decode. +type inheritableString string + +func (s *inheritableString) UnmarshalTOML(v any) error { + if str, ok := v.(string); ok { + *s = inheritableString(str) + } + return nil +} + func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result, error) { var cargo struct { Package struct { - Name string `toml:"name"` - Version string `toml:"version"` - License string `toml:"license"` - LicenseFile string `toml:"license-file"` + Name string `toml:"name"` + Version inheritableString `toml:"version"` + License inheritableString `toml:"license"` + LicenseFile inheritableString `toml:"license-file"` } `toml:"package"` Dependencies map[string]any `toml:"dependencies"` DevDependencies map[string]any `toml:"dev-dependencies"` @@ -72,13 +85,13 @@ func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result, var licenses []string if cargo.Package.License != "" { - licenses = []string{cargo.Package.License} + licenses = []string{string(cargo.Package.License)} } return &core.Result{ Name: pkgName, - Version: cargo.Package.Version, + Version: string(cargo.Package.Version), Licenses: licenses, - LicenseFile: cargo.Package.LicenseFile, + LicenseFile: string(cargo.Package.LicenseFile), Dependencies: filtered, Declarations: declarations, }, nil diff --git a/internal/cargo/cargo_test.go b/internal/cargo/cargo_test.go index 5dd4d0e..6c63322 100644 --- a/internal/cargo/cargo_test.go +++ b/internal/cargo/cargo_test.go @@ -123,6 +123,60 @@ anyhow = "=1.0.0" } } +func TestCargoTomlWorkspaceInheritedPackageFields(t *testing.T) { + content := []byte(`[package] +name = "member" +version.workspace = true +license.workspace = true +license-file = { workspace = true } +edition.workspace = true + +[dependencies] +serde = "1.0" +`) + + parser := &cargoTomlParser{} + result, err := parser.Parse("Cargo.toml", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + if result.Name != "member" { + t.Errorf("Name = %q, want %q", result.Name, "member") + } + if result.Version != "" { + t.Errorf("Version = %q, want empty for workspace-inherited", result.Version) + } + if result.Licenses != nil { + t.Errorf("Licenses = %v, want nil for workspace-inherited", result.Licenses) + } + if result.LicenseFile != "" { + t.Errorf("LicenseFile = %q, want empty for workspace-inherited", result.LicenseFile) + } + if len(result.Dependencies) != 1 || result.Dependencies[0].Name != "serde" { + t.Fatalf("Dependencies = %+v, want [serde]", result.Dependencies) + } + + literal := []byte(`[package] +name = "root" +version = "1.2.3" +license = "MIT OR Apache-2.0" +license-file = "COPYING" +`) + result, err = parser.Parse("Cargo.toml", literal) + if err != nil { + t.Fatalf("Parse literal failed: %v", err) + } + if result.Version != "1.2.3" { + t.Errorf("literal Version = %q, want %q", result.Version, "1.2.3") + } + if len(result.Licenses) != 1 || result.Licenses[0] != "MIT OR Apache-2.0" { + t.Errorf("literal Licenses = %v, want [MIT OR Apache-2.0]", result.Licenses) + } + if result.LicenseFile != "COPYING" { + t.Errorf("literal LicenseFile = %q, want %q", result.LicenseFile, "COPYING") + } +} + func TestCargoLock(t *testing.T) { content, err := os.ReadFile("../../testdata/cargo/Cargo.lock") if err != nil {