Skip to content

Commit

Permalink
feat: support parsing renv.lock files (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Feb 1, 2024
1 parent 4e8ec5a commit 95d6b6d
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ The detector supports parsing the following lockfiles:
| `Gemfile.lock` | `RubyGems` | `bundler` |
| `go.mod` | `Go` | `go mod` |
| `gradle.lockfile` | `Maven` | `gradle` |
| `gradle.lockfile` | `CRAN` | `renv` |
| `mix.lock` | `Hex` | `mix` |
| `poetry.lock` | `PyPI` | `poetry` |
| `Pipfile.lock` | `PyPI` | `pipenv` |
Expand Down
1 change: 1 addition & 0 deletions internal/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func TestReporter_PrintKnownEcosystems(t *testing.T) {

expected := strings.Join([]string{
"The detector supports parsing for the following ecosystems:",
" CRAN",
" npm",
" NuGet",
" crates.io",
Expand Down
1 change: 1 addition & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func TestRun(t *testing.T) {
poetry.lock
pom.xml
pubspec.lock
renv.lock
requirements.txt
yarn.lock
csv-file
Expand Down
1 change: 1 addition & 0 deletions pkg/lockfile/ecosystems.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package lockfile

func KnownEcosystems() []Ecosystem {
return []Ecosystem{
CRANEcosystem,
NpmEcosystem,
NuGetEcosystem,
CargoEcosystem,
Expand Down
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/renv/empty.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/renv/not-json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not json!
17 changes: 17 additions & 0 deletions pkg/lockfile/fixtures/renv/one-package.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"R": {
"Version": "2.15.2",
"Repositories": []
},
"Packages": {
"morning": {
"Package": "morning",
"Version": "0.1.0",
"Repository": "CRAN",
"Requirements": [
"coffee",
"toast"
]
}
}
}
27 changes: 27 additions & 0 deletions pkg/lockfile/fixtures/renv/two-packages.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"R": {
"Version": "4.2.3",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"markdown": {
"Package": "markdown",
"Version": "1.0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "4584a57f565dd7987d59dda3a02cfb41"
},
"mime": {
"Package": "mime",
"Version": "0.7",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "908d95ccbfd1dd274073ef07a7c93934"
}
}
}
29 changes: 29 additions & 0 deletions pkg/lockfile/fixtures/renv/with-bioconductor.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"R": {
"Version": "4.1.0",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cran.rstudio.com"
}
]
},
"Bioconductor": {
"Version": "3.13"
},
"Packages": {
"BH": {
"Package": "BH",
"Version": "1.75.0-0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "e4c04affc2cac20c8fec18385cd14691"
},
"BSgenome": {
"Package": "BSgenome",
"Version": "1.60.0",
"Source": "Bioconductor",
"Hash": "bc39f66b170caed3ea67c03eb6b4b55c"
}
}
}
35 changes: 35 additions & 0 deletions pkg/lockfile/fixtures/renv/with-mixed-sources.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"R": {
"Version": "4.3.1",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"markdown": {
"Package": "markdown",
"Version": "1.0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "4584a57f565dd7987d59dda3a02cfb41"
},
"mime": {
"Package": "mime",
"Version": "0.12.1",
"Source": "GitHub",
"RemoteType": "github",
"RemoteHost": "api.github.com",
"RemoteUsername": "yihui",
"RemoteRepo": "mime",
"RemoteRef": "main",
"RemoteSha": "1763e0dcb72fb58d97bab97bb834fc71f1e012bc",
"Requirements": [
"tools"
],
"Hash": "c2772b6269924dad6784aaa1d99dbb86"
}
}
}
16 changes: 16 additions & 0 deletions pkg/lockfile/fixtures/renv/without-repository.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"R": {
"Version": "2.15.2",
"Repositories": []
},
"Packages": {
"morning": {
"Package": "morning",
"Version": "0.1.0",
"Requirements": [
"coffee",
"toast"
]
}
}
}
54 changes: 54 additions & 0 deletions pkg/lockfile/parse-renv-lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package lockfile

import (
"encoding/json"
"fmt"
"os"
)

type RenvPackage struct {
Package string `json:"Package"`
Version string `json:"Version"`
Repository string `json:"Repository"`
}

type RenvLockfile struct {
Packages map[string]RenvPackage `json:"Packages"`
}

const CRANEcosystem Ecosystem = "CRAN"

func ParseRenvLock(pathToLockfile string) ([]PackageDetails, error) {
var parsedLockfile *RenvLockfile

lockfileContents, err := os.ReadFile(pathToLockfile)

if err != nil {
return []PackageDetails{}, fmt.Errorf("could not read %s: %w", pathToLockfile, err)
}

err = json.Unmarshal(lockfileContents, &parsedLockfile)

if err != nil {
return []PackageDetails{}, fmt.Errorf("could not parse %s: %w", pathToLockfile, err)
}

packages := make([]PackageDetails, 0, len(parsedLockfile.Packages))

for _, pkg := range parsedLockfile.Packages {
// currently we assume that unless a package is explicitly for a different
// repository, it is a CRAN package (even if its Source is not Repository)
if pkg.Repository != "" && pkg.Repository != string(CRANEcosystem) {
continue
}

packages = append(packages, PackageDetails{
Name: pkg.Package,
Version: pkg.Version,
Ecosystem: CRANEcosystem,
CompareAs: CRANEcosystem,
})
}

return packages, nil
}
124 changes: 124 additions & 0 deletions pkg/lockfile/parse-renv-lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package lockfile_test

import (
"testing"

"github.com/g-rath/osv-detector/pkg/lockfile"
)

func TestParseRenvLock_FileDoesNotExist(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/does-not-exist")

expectErrContaining(t, err, "could not read")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_InvalidJson(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/not-json.txt")

expectErrContaining(t, err, "could not parse")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_NoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/empty.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_OnePackage(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/one-package.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "morning",
Version: "0.1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_TwoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/two-packages.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "markdown",
Version: "1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
{
Name: "mime",
Version: "0.7",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_WithMixedSources(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/with-mixed-sources.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "markdown",
Version: "1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
{
Name: "mime",
Version: "0.12.1",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}
func TestParseRenvLock_WithoutRepository(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/without-repository.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "morning",
Version: "0.1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}
1 change: 1 addition & 0 deletions pkg/lockfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var parsers = map[string]PackageDetailsParser{
"Pipfile.lock": ParsePipenvLock,
"pom.xml": ParseMavenLock,
"pubspec.lock": ParsePubspecLock,
"renv.lock": ParseRenvLock,
"requirements.txt": ParseRequirementsTxt,
"yarn.lock": ParseYarnLock,
"gradle.lockfile": ParseGradleLock,
Expand Down
1 change: 1 addition & 0 deletions pkg/lockfile/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
"Gemfile.lock",
"go.mod",
"gradle.lockfile",
"renv.lock",
"mix.lock",
"pom.xml",
"poetry.lock",
Expand Down

0 comments on commit 95d6b6d

Please sign in to comment.