Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func main() {
| pre-commit | .pre-commit-config.yaml, prek.toml | |
| npm | package.json, bower.json | package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, npm-ls.json |
| nuget | *.csproj, *.vbproj, *.fsproj, *.nuspec, packages.config, Project.json | packages.lock.json, paket.lock, project.assets.json, *.deps.json, Project.lock.json |
| opam | opam, *.opam | |
| pub | pubspec.yaml | pubspec.lock |
| 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 |
| 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 |
| rpm | *.spec | |
| swift | Package.swift | Package.resolved |
| vcpkg | vcpkg.json | |
Expand Down Expand Up @@ -184,12 +185,16 @@ type ParseResult struct {
Kind Kind // manifest, lockfile, or supplement
Name string // the package's own name, when the format declares one
Version string // the package's own version, when declared
Licenses []string // raw declared license values
LicenseFile string // manifest-relative path to a declared license file
Dependencies []Dependency
}
```

`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.

`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.

### Kind

```go
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
_ "github.com/git-pkgs/manifests/internal/nix"
_ "github.com/git-pkgs/manifests/internal/npm"
_ "github.com/git-pkgs/manifests/internal/nuget"
_ "github.com/git-pkgs/manifests/internal/opam"
_ "github.com/git-pkgs/manifests/internal/precommit"
_ "github.com/git-pkgs/manifests/internal/pub"
_ "github.com/git-pkgs/manifests/internal/pypi"
Expand Down
18 changes: 15 additions & 3 deletions internal/cargo/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ type cargoTomlParser struct{}
func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result, error) {
var cargo struct {
Package struct {
Name string `toml:"name"`
Version string `toml:"version"`
Name string `toml:"name"`
Version string `toml:"version"`
License string `toml:"license"`
LicenseFile string `toml:"license-file"`
} `toml:"package"`
Dependencies map[string]any `toml:"dependencies"`
DevDependencies map[string]any `toml:"dev-dependencies"`
Expand Down Expand Up @@ -84,7 +86,17 @@ func (p *cargoTomlParser) Parse(filename string, content []byte) (*core.Result,
}
}

return &core.Result{Name: pkgName, Version: cargo.Package.Version, Dependencies: filtered}, nil
var licenses []string
if cargo.Package.License != "" {
licenses = []string{cargo.Package.License}
}
return &core.Result{
Name: pkgName,
Version: cargo.Package.Version,
Licenses: licenses,
LicenseFile: cargo.Package.LicenseFile,
Dependencies: filtered,
}, nil
}

func extractCargoVersion(value any) string {
Expand Down
26 changes: 25 additions & 1 deletion internal/cocoapods/cocoapods.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ var (
podspecNameRegex = regexp.MustCompile(`\.name\s*=\s*["']([^"']+)["']`)
// s.version = "1.0.0"
podspecVersionRegex = regexp.MustCompile(`\.version\s*=\s*["']([^"']+)["']`)
// s.license = "MIT"
podspecLicenseRegex = regexp.MustCompile(`\.license\s*=\s*["']([^"']+)["']`)
// s.license = { :type => "MIT", :file => "LICENSE" }
podspecLicenseHashRegex = regexp.MustCompile(`(?s)\.license\s*=\s*\{([^}]*)\}`)
podspecLicenseTypeRegex = regexp.MustCompile(`(?::type|["']type["']|\btype)\s*(?:=>|:)\s*["']([^"']+)["']`)
podspecLicenseFileRegex = regexp.MustCompile(`(?::file|["']file["']|\bfile)\s*(?:=>|:)\s*["']([^"']+)["']`)
)

func (p *podspecParser) Parse(filename string, content []byte) (*core.Result, error) {
Expand All @@ -183,6 +189,18 @@ func (p *podspecParser) Parse(filename string, content []byte) (*core.Result, er
if m := podspecVersionRegex.FindStringSubmatch(text); m != nil {
selfVersion = m[1]
}
var licenses []string
var licenseFile string
if m := podspecLicenseRegex.FindStringSubmatch(text); m != nil {
licenses = []string{m[1]}
} else if hash := podspecLicenseHashRegex.FindStringSubmatch(text); hash != nil {
if m := podspecLicenseTypeRegex.FindStringSubmatch(hash[1]); m != nil {
licenses = []string{m[1]}
}
if m := podspecLicenseFileRegex.FindStringSubmatch(hash[1]); m != nil {
licenseFile = m[1]
}
}

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

return &core.Result{Name: selfName, Version: selfVersion, Dependencies: deps}, nil
return &core.Result{
Name: selfName,
Version: selfVersion,
Licenses: licenses,
LicenseFile: licenseFile,
Dependencies: deps,
}, nil
}
26 changes: 25 additions & 1 deletion internal/composer/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type composerJSONParser struct{}
type composerJSON struct {
Name string `json:"name"`
Version string `json:"version"`
License any `json:"license"`
Require map[string]string `json:"require"`
RequireDev map[string]string `json:"require-dev"`
}
Expand Down Expand Up @@ -59,7 +60,30 @@ func (p *composerJSONParser) Parse(filename string, content []byte) (*core.Resul
})
}

return &core.Result{Name: composer.Name, Version: composer.Version, Dependencies: deps}, nil
return &core.Result{
Name: composer.Name,
Version: composer.Version,
Licenses: composerLicenses(composer.License),
Dependencies: deps,
}, nil
}

func composerLicenses(value any) []string {
switch license := value.(type) {
case string:
if license != "" {
return []string{license}
}
case []any:
licenses := make([]string, 0, len(license))
for _, item := range license {
if text, ok := item.(string); ok && text != "" {
licenses = append(licenses, text)
}
}
return licenses
}
return nil
}

// composerLockParser parses composer.lock files.
Expand Down
7 changes: 6 additions & 1 deletion internal/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ type Result struct {
// that only list dependencies (Gemfile, requirements.txt, etc.).
Name string
// Version is the package's own version as declared in the manifest.
Version string
Version string
// Licenses holds the package's declared license values, without
// normalization.
Licenses []string
// LicenseFile is a manifest-relative path to a declared license file.
LicenseFile string
Dependencies []Dependency
}

Expand Down
34 changes: 33 additions & 1 deletion internal/cran/cran.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,39 @@ func (p *descriptionParser) Parse(filename string, content []byte) (*core.Result
}
}

return &core.Result{Name: fields["Package"], Version: fields["Version"], Dependencies: deps}, nil
licenses, licenseFile := parseRLicense(fields["License"])
return &core.Result{
Name: fields["Package"],
Version: fields["Version"],
Licenses: licenses,
LicenseFile: licenseFile,
Dependencies: deps,
}, nil
}

func parseRLicense(value string) ([]string, string) {
var licenses []string
var licenseFile string
for _, alternative := range strings.Split(value, "|") {
alternative = strings.TrimSpace(alternative)
if alternative == "" {
continue
}
lower := strings.ToLower(alternative)
if idx := strings.Index(lower, "file "); idx >= 0 {
if licenseFile == "" {
fileFields := strings.Fields(alternative[idx+len("file "):])
if len(fileFields) > 0 {
licenseFile = fileFields[0]
}
}
alternative = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(alternative[:idx]), "+"))
}
if alternative != "" {
licenses = append(licenses, alternative)
}
}
return licenses, licenseFile
}

// parseDescriptionFields parses DESCRIPTION file key-value pairs.
Expand Down
98 changes: 64 additions & 34 deletions internal/elm/elm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type elmJSONParser struct{}
type elmJSON struct {
Name string `json:"name"`
Version string `json:"version"`
Dependencies elmDependencies `json:"dependencies"`
TestDependencies elmDependencies `json:"test-dependencies"`
License string `json:"license"`
Dependencies json.RawMessage `json:"dependencies"`
TestDependencies json.RawMessage `json:"test-dependencies"`
}

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

var deps []core.Dependency
deps, err := parseElmDependencies(elm.Dependencies, core.Runtime)
if err != nil {
return nil, &core.ParseError{Filename: filename, Err: err}
}
testDeps, err := parseElmDependencies(elm.TestDependencies, core.Test)
if err != nil {
return nil, &core.ParseError{Filename: filename, Err: err}
}
deps = append(deps, testDeps...)

return &core.Result{
Name: elm.Name,
Version: elm.Version,
Licenses: declaredLicense(elm.License),
Dependencies: deps,
}, nil
}

// Direct dependencies
for name, version := range elm.Dependencies.Direct {
deps = append(deps, core.Dependency{
Name: name,
Version: version,
Scope: core.Runtime,
Direct: true,
})
func parseElmDependencies(content json.RawMessage, scope core.Scope) ([]core.Dependency, error) {
if len(content) == 0 || string(content) == "null" {
return nil, nil
}

// Indirect dependencies
for name, version := range elm.Dependencies.Indirect {
deps = append(deps, core.Dependency{
Name: name,
Version: version,
Scope: core.Runtime,
Direct: false,
})
var shape map[string]json.RawMessage
if err := json.Unmarshal(content, &shape); err != nil {
return nil, err
}
_, hasDirect := shape["direct"]
_, hasIndirect := shape["indirect"]
if hasDirect || hasIndirect {
var groups elmDependencies
if err := json.Unmarshal(content, &groups); err != nil {
return nil, err
}
deps := make([]core.Dependency, 0, len(groups.Direct)+len(groups.Indirect))
deps = appendElmDependencies(deps, groups.Direct, scope, true)
deps = appendElmDependencies(deps, groups.Indirect, scope, false)
return deps, nil
}

// Test dependencies (direct)
for name, version := range elm.TestDependencies.Direct {
deps = append(deps, core.Dependency{
Name: name,
Version: version,
Scope: core.Test,
Direct: true,
})
var packages map[string]string
if err := json.Unmarshal(content, &packages); err != nil {
return nil, err
}
return appendElmDependencies(nil, packages, scope, true), nil
}

// Test dependencies (indirect)
for name, version := range elm.TestDependencies.Indirect {
func appendElmDependencies(deps []core.Dependency, packages map[string]string, scope core.Scope, direct bool) []core.Dependency {
for name, version := range packages {
deps = append(deps, core.Dependency{
Name: name,
Version: version,
Scope: core.Test,
Direct: false,
Scope: scope,
Direct: direct,
})
}

return &core.Result{Name: elm.Name, Version: elm.Version, Dependencies: deps}, nil
return deps
}

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

type elmPackageJSON struct {
Name string `json:"name"`
Version string `json:"version"`
License string `json:"license"`
Dependencies map[string]string `json:"dependencies"`
}

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

return &core.Result{Dependencies: deps}, nil
return &core.Result{
Name: elm.Name,
Version: elm.Version,
Licenses: declaredLicense(elm.License),
Dependencies: deps,
}, nil
}

func declaredLicense(value string) []string {
if value == "" {
return nil
}
return []string{value}
}
Loading