feat: add support for authors, maintainers, and contributors in package.json. (#4003)

Fixes #2250
---------
Signed-off-by: Alan Pope <alan.pope@anchore.com>
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
This commit is contained in:
Alan Pope 2025-08-13 22:55:15 +01:00 committed by GitHub
parent ab9db0024e
commit 87e1d8cb87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 347 additions and 13 deletions

View File

@ -25,6 +25,39 @@ func newPackageJSONPackage(ctx context.Context, u packageJSON, indexLocation fil
} }
license := pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, licenseCandidates...) license := pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, licenseCandidates...)
// Handle author, authors, contributors, and maintainers fields
var authorParts []string
// Add a single author field if it exists
if u.Author.Name != "" || u.Author.Email != "" || u.Author.URL != "" {
if authStr := u.Author.AuthorString(); authStr != "" {
authorParts = append(authorParts, authStr)
}
}
// Add authors field if it exists
if len(u.Authors) > 0 {
if authorsStr := u.Authors.String(); authorsStr != "" {
authorParts = append(authorParts, authorsStr)
}
}
// Add contributors field if it exists
if len(u.Contributors) > 0 {
if contributorsStr := u.Contributors.String(); contributorsStr != "" {
authorParts = append(authorParts, contributorsStr)
}
}
// Add maintainers field if it exists
if len(u.Maintainers) > 0 {
if maintainersStr := u.Maintainers.String(); maintainersStr != "" {
authorParts = append(authorParts, maintainersStr)
}
}
authorInfo := strings.Join(authorParts, ", ")
p := pkg.Package{ p := pkg.Package{
Name: u.Name, Name: u.Name,
Version: u.Version, Version: u.Version,
@ -37,7 +70,7 @@ func newPackageJSONPackage(ctx context.Context, u packageJSON, indexLocation fil
Name: u.Name, Name: u.Name,
Version: u.Version, Version: u.Version,
Description: u.Description, Description: u.Description,
Author: u.Author.AuthorString(), Author: authorInfo,
Homepage: u.Homepage, Homepage: u.Homepage,
URL: u.Repository.URL, URL: u.Repository.URL,
Private: u.Private, Private: u.Private,

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"regexp" "regexp"
"strings"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
@ -24,7 +25,10 @@ var _ generic.Parser = parsePackageJSON
type packageJSON struct { type packageJSON struct {
Version string `json:"version"` Version string `json:"version"`
Latest []string `json:"latest"` Latest []string `json:"latest"`
Author author `json:"author"` Author person `json:"author"`
Authors people `json:"authors"`
Contributors people `json:"contributors"`
Maintainers people `json:"maintainers"`
License json.RawMessage `json:"license"` License json.RawMessage `json:"license"`
Licenses json.RawMessage `json:"licenses"` Licenses json.RawMessage `json:"licenses"`
Name string `json:"name"` Name string `json:"name"`
@ -35,12 +39,14 @@ type packageJSON struct {
Private bool `json:"private"` Private bool `json:"private"`
} }
type author struct { type person struct {
Name string `json:"name" mapstructure:"name"` Name string `json:"name" mapstructure:"name"`
Email string `json:"email" mapstructure:"email"` Email string `json:"email" mapstructure:"email"`
URL string `json:"url" mapstructure:"url"` URL string `json:"url" mapstructure:"url"`
} }
type people []person
type repository struct { type repository struct {
Type string `json:"type" mapstructure:"type"` Type string `json:"type" mapstructure:"type"`
URL string `json:"url" mapstructure:"url"` URL string `json:"url" mapstructure:"url"`
@ -76,9 +82,9 @@ func parsePackageJSON(ctx context.Context, _ file.Resolver, _ *generic.Environme
return pkgs, nil, nil return pkgs, nil, nil
} }
func (a *author) UnmarshalJSON(b []byte) error { func (p *person) UnmarshalJSON(b []byte) error {
var authorStr string var authorStr string
var auth author var auth person
if err := json.Unmarshal(b, &authorStr); err == nil { if err := json.Unmarshal(b, &authorStr); err == nil {
// successfully parsed as a string, now parse that string into fields // successfully parsed as a string, now parse that string into fields
@ -97,18 +103,18 @@ func (a *author) UnmarshalJSON(b []byte) error {
} }
} }
*a = auth *p = auth
return nil return nil
} }
func (a *author) AuthorString() string { func (p *person) AuthorString() string {
result := a.Name result := p.Name
if a.Email != "" { if p.Email != "" {
result += fmt.Sprintf(" <%s>", a.Email) result += fmt.Sprintf(" <%s>", p.Email)
} }
if a.URL != "" { if p.URL != "" {
result += fmt.Sprintf(" (%s)", a.URL) result += fmt.Sprintf(" (%s)", p.URL)
} }
return result return result
} }
@ -210,3 +216,58 @@ func pathContainsNodeModulesDirectory(p string) bool {
} }
return false return false
} }
func (p *people) UnmarshalJSON(b []byte) error {
// Try to unmarshal as an array of strings
var authorStrings []string
if err := json.Unmarshal(b, &authorStrings); err == nil {
// Successfully parsed as an array of strings
auths := make([]person, len(authorStrings))
for i, authorStr := range authorStrings {
// Parse each string into author fields
fields := internal.MatchNamedCaptureGroups(authorPattern, authorStr)
var auth person
if err := mapstructure.Decode(fields, &auth); err != nil {
return fmt.Errorf("unable to decode package.json author: %w", err)
}
// Trim whitespace from name if it was parsed
if auth.Name != "" {
auth.Name = strings.TrimSpace(auth.Name)
}
auths[i] = auth
}
*p = auths
return nil
}
// Try to unmarshal as an array of objects
var authorObjs []map[string]interface{}
if err := json.Unmarshal(b, &authorObjs); err == nil {
// Successfully parsed as an array of objects
auths := make([]person, len(authorObjs))
for i, fields := range authorObjs {
var auth person
if err := mapstructure.Decode(fields, &auth); err != nil {
return fmt.Errorf("unable to decode package.json author object: %w", err)
}
auths[i] = auth
}
*p = auths
return nil
}
// If we get here, it means neither format matched
return fmt.Errorf("unable to parse package.json authors field: expected array of strings or array of objects")
}
func (p people) String() string {
if len(p) == 0 {
return ""
}
authorStrings := make([]string, len(p))
for i, auth := range p {
authorStrings[i] = auth.AuthorString()
}
return strings.Join(authorStrings, ", ")
}

View File

@ -152,7 +152,7 @@ func TestParsePackageJSON(t *testing.T) {
Metadata: pkg.NpmPackage{ Metadata: pkg.NpmPackage{
Name: "function-bind", Name: "function-bind",
Version: "1.1.1", Version: "1.1.1",
Author: "Raynos <raynos2@gmail.com>", Author: "Raynos <raynos2@gmail.com>, Raynos, Jordan Harband (https://github.com/ljharb)",
Homepage: "https://github.com/Raynos/function-bind", Homepage: "https://github.com/Raynos/function-bind",
URL: "git://github.com/Raynos/function-bind.git", URL: "git://github.com/Raynos/function-bind.git",
Description: "Implementation of Function.prototype.bind", Description: "Implementation of Function.prototype.bind",
@ -202,6 +202,132 @@ func TestParsePackageJSON(t *testing.T) {
}, },
}, },
}, },
{
Fixture: "test-fixtures/pkg-json/package-authors-array.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-authors-array.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Harry Potter <hp@hogwards.com> (http://youknowwho.com/), John Smith <j.smith@something.com> (http://awebsite.com/)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-authors-objects.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-authors-objects.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Harry Potter <hp@hogwards.com> (http://youknowwho.com/), John Smith <j.smith@something.com> (http://awebsite.com/)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-both-author-and-authors.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-both-author-and-authors.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me), Harry Potter <hp@hogwards.com> (http://youknowwho.com/), John Smith <j.smith@something.com> (http://awebsite.com/)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-contributors.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-contributors.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Alice Contributor <alice@example.com>, Bob Helper <bob@example.com>",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-maintainers.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-maintainers.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Charlie Maintainer <charlie@example.com>, Diana Keeper <diana@example.com>",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-all-author-fields.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
PURL: "pkg:npm/npm@6.14.6",
Type: pkg.NpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-all-author-fields.json")),
),
Language: pkg.JavaScript,
Metadata: pkg.NpmPackage{
Name: "npm",
Version: "6.14.6",
Author: "Main Author <main@example.com>, Second Author <second@example.com>, Contrib One <contrib1@example.com>, Maintainer One <maintain1@example.com>",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Description: "a package manager for JavaScript",
},
},
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -0,0 +1,24 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"author": "Main Author <main@example.com>",
"authors": [
"Second Author <second@example.com>"
],
"contributors": [
"Contrib One <contrib1@example.com>"
],
"maintainers": [
{
"name": "Maintainer One",
"email": "maintain1@example.com"
}
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -0,0 +1,15 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"authors": [
"Harry Potter <hp@hogwards.com> (http://youknowwho.com/)",
"John Smith <j.smith@something.com> (http://awebsite.com/)"
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -0,0 +1,23 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"authors": [
{
"name": "Harry Potter",
"email": "hp@hogwards.com",
"url": "http://youknowwho.com/"
},
{
"name": "John Smith",
"email": "j.smith@something.com",
"url": "http://awebsite.com/"
}
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -0,0 +1,16 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
"authors": [
"Harry Potter <hp@hogwards.com> (http://youknowwho.com/)",
"John Smith <j.smith@something.com> (http://awebsite.com/)"
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -0,0 +1,15 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"contributors": [
"Alice Contributor <alice@example.com>",
"Bob Helper <bob@example.com>"
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -0,0 +1,21 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"maintainers": [
{
"name": "Charlie Maintainer",
"email": "charlie@example.com"
},
{
"name": "Diana Keeper",
"email": "diana@example.com"
}
],
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}