diff --git a/internal/ecosystem/yarn/yarn.go b/internal/ecosystem/yarn/yarn.go index 8eb3f34..8000820 100644 --- a/internal/ecosystem/yarn/yarn.go +++ b/internal/ecosystem/yarn/yarn.go @@ -157,7 +157,11 @@ func parseYarnLock(data []byte) []yarnEntry { continue } line := strings.TrimSpace(raw) - if strings.HasPrefix(line, "version ") || strings.HasPrefix(line, "version:") { + // First-wins: yarn (Classic and Berry) always emits the entry's own + // version as the first field, so any later match is a dependency + // literally named "version" inside a dependencies:/peerDependencies: + // block, not the entry version. + if (strings.HasPrefix(line, "version ") || strings.HasPrefix(line, "version:")) && cur.version == "" { cur.version = unquote(trimField(line, "version")) } } diff --git a/internal/ecosystem/yarn/yarn_test.go b/internal/ecosystem/yarn/yarn_test.go index a826da6..ebb26a7 100644 --- a/internal/ecosystem/yarn/yarn_test.go +++ b/internal/ecosystem/yarn/yarn_test.go @@ -212,3 +212,66 @@ __metadata: t.Errorf("lodash: %+v", out[0]) } } + +// TestScanLockfile_DependencyNamedVersion guards against a dependency +// literally named "version" (a real npm package) inside a +// dependencies:/peerDependencies: block overwriting the entry's own +// version, which yarn always emits as the first field. +func TestScanLockfile_DependencyNamedVersion(t *testing.T) { + cases := []struct { + name string + body string + }{ + { + name: "classic", + body: `# yarn lockfile v1 + +board2d@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/board2d/-/board2d-1.0.0.tgz" + integrity sha512-xyz + dependencies: + version "^0.1.0" +`, + }, + { + name: "berry", + body: `# This file is generated by running "yarn install" inside your project. + +__metadata: + version: 6 + cacheKey: 8 + +"board2d@npm:^1.0.0": + version: 1.0.0 + resolution: "board2d@npm:1.0.0" + dependencies: + version: "npm:^0.1.0" + checksum: 10/abc + languageName: node + linkType: hard +`, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "yarn.lock") + if err := os.WriteFile(path, []byte(c.body), 0o644); err != nil { + t.Fatal(err) + } + var out []model.Record + s := &Scanner{MaxFileSize: 1 << 20, Emit: func(r model.Record) { out = append(out, r) }} + if err := s.ScanLockfile(path, model.Record{}); err != nil { + t.Fatal(err) + } + if len(out) != 1 { + t.Fatalf("want 1 record, got %d", len(out)) + } + if out[0].PackageName != "board2d" || out[0].Version != "1.0.0" { + t.Errorf("board2d: got name=%q version=%q, want name=%q version=%q", + out[0].PackageName, out[0].Version, "board2d", "1.0.0") + } + }) + } +}