fix: Allow duplicates in Yarn "Berry" files (#4691) (#4838)

* fix: Allow duplicates in Yarn "Berry" files (#4691)

Yarn lockfiles can have multiple versions resolved for the same package
name. We correctly allow this in Yarn v1 lockfiles but the "Berry"
YAML-format lockfiles were doing deduplication by package name. This
change removes that deduplication.

Signed-off-by: Calum Leslie <cleslie@atlassian.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Calum Leslie <cleslie@atlassian.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Calum Leslie <cleslie@atlassian.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Calum Leslie 2026-05-11 22:10:17 +01:00 committed by GitHub
parent dfb6011083
commit 36969bdeff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 5 deletions

View File

@ -6,9 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"maps"
"regexp" "regexp"
"slices"
"strings" "strings"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
@ -250,7 +248,9 @@ func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) {
return nil, fmt.Errorf("failed to unmarshal yarn v2 lockfile: %w", err) return nil, fmt.Errorf("failed to unmarshal yarn v2 lockfile: %w", err)
} }
packages := make(map[string]yarnPackage) var seenPkgs = strset.New()
pkgs := []yarnPackage{}
for key, value := range lockfile { for key, value := range lockfile {
packageName := findPackageName(key) packageName := findPackageName(key)
if packageName == "" { if packageName == "" {
@ -258,10 +258,16 @@ func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) {
continue continue
} }
packages[packageName] = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies} var pkg = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies}
var nameVersion = pkg.Name + "@" + pkg.Version
if !seenPkgs.Has(nameVersion) {
seenPkgs.Add(nameVersion)
pkgs = append(pkgs, pkg)
}
} }
return slices.Collect(maps.Values(packages)), nil return pkgs, nil
} }
func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {

View File

@ -204,6 +204,42 @@ func TestParseYarnBerry(t *testing.T) {
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships) pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships)
} }
func TestParseYarnBerryWithDuplicates(t *testing.T) {
var expectedRelationships []artifact.Relationship
fixture := "testdata/yarn-berry-dups/yarn.lock"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "async",
Version: "3.2.6",
Locations: locations,
PURL: "pkg:npm/async@3.2.6",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "async@npm:3.2.6",
Integrity: "10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70",
},
},
{
Name: "async",
Version: "0.9.2",
Locations: locations,
PURL: "pkg:npm/async@0.9.2",
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
Metadata: pkg.YarnLockEntry{
Resolved: "async@npm:0.9.2",
Integrity: "10c0/22ac816db119a9b84ac7182fa969b2cceacfcfa278c3efb0ac6a94d1210a4429e42c8cf6e704039aa7662e4ba62f26cecf039c91d41ceb91355dc9672c9b9ac1",
},
},
}
adapter := newGenericYarnLockAdapter(CatalogerConfig{})
pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships)
}
func TestParseYarnLock(t *testing.T) { func TestParseYarnLock(t *testing.T) {
var expectedRelationships []artifact.Relationship var expectedRelationships []artifact.Relationship
fixture := "testdata/yarn/yarn.lock" fixture := "testdata/yarn/yarn.lock"

View File

@ -0,0 +1,20 @@
# This file is generated by running "yarn install" inside your project.
# Manual changes might be lost - proceed with caution!
__metadata:
version: 8
cacheKey: 10c0
"async@npm:^3.2.3":
version: 3.2.6
resolution: "async@npm:3.2.6"
checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70
languageName: node
linkType: hard
"async@npm:0.9.2":
version: 0.9.2
resolution: "async@npm:0.9.2"
checksum: 10c0/22ac816db119a9b84ac7182fa969b2cceacfcfa278c3efb0ac6a94d1210a4429e42c8cf6e704039aa7662e4ba62f26cecf039c91d41ceb91355dc9672c9b9ac1
languageName: node
linkType: hard