Skip to content

Commit eb09b6d

Browse files
Return declared licenses from ParseResult (#50)
* Return declared licenses from ParseResult * Fix declared license parser edge cases * Address declared license review feedback
1 parent ec52417 commit eb09b6d

24 files changed

Lines changed: 1387 additions & 57 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ func main() {
7676
| pre-commit | .pre-commit-config.yaml, prek.toml | |
7777
| npm | package.json, bower.json | package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, npm-ls.json |
7878
| nuget | *.csproj, *.vbproj, *.fsproj, *.nuspec, packages.config, Project.json | packages.lock.json, paket.lock, project.assets.json, *.deps.json, Project.lock.json |
79+
| opam | opam, *.opam | |
7980
| pub | pubspec.yaml | pubspec.lock |
80-
| pypi | requirements.txt, Pipfile, pyproject.toml, setup.py | Pipfile.lock, poetry.lock, pdm.lock, uv.lock, pip-dependency-graph.json, pip-resolved-dependencies.txt, pylock.toml |
81+
| pypi | requirements.txt, Pipfile, pyproject.toml, setup.py, setup.cfg | Pipfile.lock, poetry.lock, pdm.lock, uv.lock, pip-dependency-graph.json, pip-resolved-dependencies.txt, pylock.toml |
8182
| rpm | *.spec | |
8283
| swift | Package.swift | Package.resolved |
8384
| vcpkg | vcpkg.json | |
@@ -184,12 +185,16 @@ type ParseResult struct {
184185
Kind Kind // manifest, lockfile, or supplement
185186
Name string // the package's own name, when the format declares one
186187
Version string // the package's own version, when declared
188+
Licenses []string // raw declared license values
189+
LicenseFile string // manifest-relative path to a declared license file
187190
Dependencies []Dependency
188191
}
189192
```
190193

191194
`Name` and `Version` are populated for manifest formats that declare their own package identity (Cargo.toml `[package]`, package.json `"name"`, go.mod `module`, `.gemspec`, and so on). They are empty for lockfiles and for dependency-only files like Gemfile or requirements.txt.
192195

196+
`Licenses` contains decoded values as declared by the manifest; it does not normalize them into SPDX expressions. `LicenseFile` is populated when a format explicitly identifies a license file. Both are empty for formats without license metadata.
197+
193198
### Kind
194199

195200
```go

imports.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
_ "github.com/git-pkgs/manifests/internal/nix"
3939
_ "github.com/git-pkgs/manifests/internal/npm"
4040
_ "github.com/git-pkgs/manifests/internal/nuget"
41+
_ "github.com/git-pkgs/manifests/internal/opam"
4142
_ "github.com/git-pkgs/manifests/internal/precommit"
4243
_ "github.com/git-pkgs/manifests/internal/pub"
4344
_ "github.com/git-pkgs/manifests/internal/pypi"

internal/cargo/cargo.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ type cargoTomlParser struct{}
2121
func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result, error) {
2222
var cargo struct {
2323
Package struct {
24-
Name string `toml:"name"`
25-
Version string `toml:"version"`
24+
Name string `toml:"name"`
25+
Version string `toml:"version"`
26+
License string `toml:"license"`
27+
LicenseFile string `toml:"license-file"`
2628
} `toml:"package"`
2729
Dependencies map[string]any `toml:"dependencies"`
2830
DevDependencies map[string]any `toml:"dev-dependencies"`
@@ -84,7 +86,17 @@ func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result,
8486
}
8587
}
8688

87-
return &core.Result{Name: pkgName, Version: cargo.Package.Version, Dependencies: filtered}, nil
89+
var licenses []string
90+
if cargo.Package.License != "" {
91+
licenses = []string{cargo.Package.License}
92+
}
93+
return &core.Result{
94+
Name: pkgName,
95+
Version: cargo.Package.Version,
96+
Licenses: licenses,
97+
LicenseFile: cargo.Package.LicenseFile,
98+
Dependencies: filtered,
99+
}, nil
88100
}
89101

90102
func extractCargoVersion(value any) string {

internal/cocoapods/cocoapods.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ var (
170170
podspecNameRegex = regexp.MustCompile(`\.name\s*=\s*["']([^"']+)["']`)
171171
// s.version = "1.0.0"
172172
podspecVersionRegex = regexp.MustCompile(`\.version\s*=\s*["']([^"']+)["']`)
173+
// s.license = "MIT"
174+
podspecLicenseRegex = regexp.MustCompile(`\.license\s*=\s*["']([^"']+)["']`)
175+
// s.license = { :type => "MIT", :file => "LICENSE" }
176+
podspecLicenseHashRegex = regexp.MustCompile(`(?s)\.license\s*=\s*\{([^}]*)\}`)
177+
podspecLicenseTypeRegex = regexp.MustCompile(`(?::type|["']type["']|\btype)\s*(?:=>|:)\s*["']([^"']+)["']`)
178+
podspecLicenseFileRegex = regexp.MustCompile(`(?::file|["']file["']|\bfile)\s*(?:=>|:)\s*["']([^"']+)["']`)
173179
)
174180

175181
func (p *podspecParser) Parse(filename string, content []byte) (*core.Result, error) {
@@ -183,6 +189,18 @@ func (p *podspecParser) Parse(filename string, content []byte) (*core.Result, er
183189
if m := podspecVersionRegex.FindStringSubmatch(text); m != nil {
184190
selfVersion = m[1]
185191
}
192+
var licenses []string
193+
var licenseFile string
194+
if m := podspecLicenseRegex.FindStringSubmatch(text); m != nil {
195+
licenses = []string{m[1]}
196+
} else if hash := podspecLicenseHashRegex.FindStringSubmatch(text); hash != nil {
197+
if m := podspecLicenseTypeRegex.FindStringSubmatch(hash[1]); m != nil {
198+
licenses = []string{m[1]}
199+
}
200+
if m := podspecLicenseFileRegex.FindStringSubmatch(hash[1]); m != nil {
201+
licenseFile = m[1]
202+
}
203+
}
186204

187205
for _, match := range podspecDepRegex.FindAllStringSubmatch(text, -1) {
188206
const versionGroup = 2
@@ -199,5 +217,11 @@ func (p *podspecParser) Parse(filename string, content []byte) (*core.Result, er
199217
})
200218
}
201219

202-
return &core.Result{Name: selfName, Version: selfVersion, Dependencies: deps}, nil
220+
return &core.Result{
221+
Name: selfName,
222+
Version: selfVersion,
223+
Licenses: licenses,
224+
LicenseFile: licenseFile,
225+
Dependencies: deps,
226+
}, nil
203227
}

internal/composer/composer.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type composerJSONParser struct{}
1616
type composerJSON struct {
1717
Name string `json:"name"`
1818
Version string `json:"version"`
19+
License any `json:"license"`
1920
Require map[string]string `json:"require"`
2021
RequireDev map[string]string `json:"require-dev"`
2122
}
@@ -59,7 +60,30 @@ func (p *composerJSONParser) Parse(filename string, content []byte) (*core.Resul
5960
})
6061
}
6162

62-
return &core.Result{Name: composer.Name, Version: composer.Version, Dependencies: deps}, nil
63+
return &core.Result{
64+
Name: composer.Name,
65+
Version: composer.Version,
66+
Licenses: composerLicenses(composer.License),
67+
Dependencies: deps,
68+
}, nil
69+
}
70+
71+
func composerLicenses(value any) []string {
72+
switch license := value.(type) {
73+
case string:
74+
if license != "" {
75+
return []string{license}
76+
}
77+
case []any:
78+
licenses := make([]string, 0, len(license))
79+
for _, item := range license {
80+
if text, ok := item.(string); ok && text != "" {
81+
licenses = append(licenses, text)
82+
}
83+
}
84+
return licenses
85+
}
86+
return nil
6387
}
6488

6589
// composerLockParser parses composer.lock files.

internal/core/types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ type Result struct {
3939
// that only list dependencies (Gemfile, requirements.txt, etc.).
4040
Name string
4141
// Version is the package's own version as declared in the manifest.
42-
Version string
42+
Version string
43+
// Licenses holds the package's declared license values, without
44+
// normalization.
45+
Licenses []string
46+
// LicenseFile is a manifest-relative path to a declared license file.
47+
LicenseFile string
4348
Dependencies []Dependency
4449
}
4550

internal/cran/cran.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,39 @@ func (p *descriptionParser) Parse(filename string, content []byte) (*core.Result
7070
}
7171
}
7272

73-
return &core.Result{Name: fields["Package"], Version: fields["Version"], Dependencies: deps}, nil
73+
licenses, licenseFile := parseRLicense(fields["License"])
74+
return &core.Result{
75+
Name: fields["Package"],
76+
Version: fields["Version"],
77+
Licenses: licenses,
78+
LicenseFile: licenseFile,
79+
Dependencies: deps,
80+
}, nil
81+
}
82+
83+
func parseRLicense(value string) ([]string, string) {
84+
var licenses []string
85+
var licenseFile string
86+
for _, alternative := range strings.Split(value, "|") {
87+
alternative = strings.TrimSpace(alternative)
88+
if alternative == "" {
89+
continue
90+
}
91+
lower := strings.ToLower(alternative)
92+
if idx := strings.Index(lower, "file "); idx >= 0 {
93+
if licenseFile == "" {
94+
fileFields := strings.Fields(alternative[idx+len("file "):])
95+
if len(fileFields) > 0 {
96+
licenseFile = fileFields[0]
97+
}
98+
}
99+
alternative = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(alternative[:idx]), "+"))
100+
}
101+
if alternative != "" {
102+
licenses = append(licenses, alternative)
103+
}
104+
}
105+
return licenses, licenseFile
74106
}
75107

76108
// parseDescriptionFields parses DESCRIPTION file key-value pairs.

internal/elm/elm.go

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ type elmJSONParser struct{}
1616
type elmJSON struct {
1717
Name string `json:"name"`
1818
Version string `json:"version"`
19-
Dependencies elmDependencies `json:"dependencies"`
20-
TestDependencies elmDependencies `json:"test-dependencies"`
19+
License string `json:"license"`
20+
Dependencies json.RawMessage `json:"dependencies"`
21+
TestDependencies json.RawMessage `json:"test-dependencies"`
2122
}
2223

2324
type elmDependencies struct {
@@ -31,55 +32,72 @@ func (p *elmJSONParser) Parse(filename string, content []byte) (*core.Result, er
3132
return nil, &core.ParseError{Filename: filename, Err: err}
3233
}
3334

34-
var deps []core.Dependency
35+
deps, err := parseElmDependencies(elm.Dependencies, core.Runtime)
36+
if err != nil {
37+
return nil, &core.ParseError{Filename: filename, Err: err}
38+
}
39+
testDeps, err := parseElmDependencies(elm.TestDependencies, core.Test)
40+
if err != nil {
41+
return nil, &core.ParseError{Filename: filename, Err: err}
42+
}
43+
deps = append(deps, testDeps...)
44+
45+
return &core.Result{
46+
Name: elm.Name,
47+
Version: elm.Version,
48+
Licenses: declaredLicense(elm.License),
49+
Dependencies: deps,
50+
}, nil
51+
}
3552

36-
// Direct dependencies
37-
for name, version := range elm.Dependencies.Direct {
38-
deps = append(deps, core.Dependency{
39-
Name: name,
40-
Version: version,
41-
Scope: core.Runtime,
42-
Direct: true,
43-
})
53+
func parseElmDependencies(content json.RawMessage, scope core.Scope) ([]core.Dependency, error) {
54+
if len(content) == 0 || string(content) == "null" {
55+
return nil, nil
4456
}
4557

46-
// Indirect dependencies
47-
for name, version := range elm.Dependencies.Indirect {
48-
deps = append(deps, core.Dependency{
49-
Name: name,
50-
Version: version,
51-
Scope: core.Runtime,
52-
Direct: false,
53-
})
58+
var shape map[string]json.RawMessage
59+
if err := json.Unmarshal(content, &shape); err != nil {
60+
return nil, err
61+
}
62+
_, hasDirect := shape["direct"]
63+
_, hasIndirect := shape["indirect"]
64+
if hasDirect || hasIndirect {
65+
var groups elmDependencies
66+
if err := json.Unmarshal(content, &groups); err != nil {
67+
return nil, err
68+
}
69+
deps := make([]core.Dependency, 0, len(groups.Direct)+len(groups.Indirect))
70+
deps = appendElmDependencies(deps, groups.Direct, scope, true)
71+
deps = appendElmDependencies(deps, groups.Indirect, scope, false)
72+
return deps, nil
5473
}
5574

56-
// Test dependencies (direct)
57-
for name, version := range elm.TestDependencies.Direct {
58-
deps = append(deps, core.Dependency{
59-
Name: name,
60-
Version: version,
61-
Scope: core.Test,
62-
Direct: true,
63-
})
75+
var packages map[string]string
76+
if err := json.Unmarshal(content, &packages); err != nil {
77+
return nil, err
6478
}
79+
return appendElmDependencies(nil, packages, scope, true), nil
80+
}
6581

66-
// Test dependencies (indirect)
67-
for name, version := range elm.TestDependencies.Indirect {
82+
func appendElmDependencies(deps []core.Dependency, packages map[string]string, scope core.Scope, direct bool) []core.Dependency {
83+
for name, version := range packages {
6884
deps = append(deps, core.Dependency{
6985
Name: name,
7086
Version: version,
71-
Scope: core.Test,
72-
Direct: false,
87+
Scope: scope,
88+
Direct: direct,
7389
})
7490
}
75-
76-
return &core.Result{Name: elm.Name, Version: elm.Version, Dependencies: deps}, nil
91+
return deps
7792
}
7893

7994
// elmPackageJSONParser parses elm-package.json files (Elm 0.18 and earlier).
8095
type elmPackageJSONParser struct{}
8196

8297
type elmPackageJSON struct {
98+
Name string `json:"name"`
99+
Version string `json:"version"`
100+
License string `json:"license"`
83101
Dependencies map[string]string `json:"dependencies"`
84102
}
85103

@@ -100,5 +118,17 @@ func (p *elmPackageJSONParser) Parse(filename string, content []byte) (*core.Res
100118
})
101119
}
102120

103-
return &core.Result{Dependencies: deps}, nil
121+
return &core.Result{
122+
Name: elm.Name,
123+
Version: elm.Version,
124+
Licenses: declaredLicense(elm.License),
125+
Dependencies: deps,
126+
}, nil
127+
}
128+
129+
func declaredLicense(value string) []string {
130+
if value == "" {
131+
return nil
132+
}
133+
return []string{value}
104134
}

0 commit comments

Comments
 (0)