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
86 changes: 86 additions & 0 deletions internal/parsers/cargo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package parsers

import "strings"

// Cargo handles `cargo add` and `cargo install`.
// cargo add serde → direct
// cargo add serde@1.0.150 → direct, pinned (@ form, cargo ≥ 1.62)
// cargo add serde --vers 1.0 → direct, pinned (--vers form)
// cargo add serde --features derive → direct
// cargo install ripgrep → direct (binary crate)
// cargo build / test / run / ... → passthrough
//
// `cargo install --path .` / `--git <url>` install from local source or a
// repo; the gate has nothing useful to say, so those positionals are skipped.

var cargoFlagsTakingArg = map[string]bool{
"--vers": true,
"--version": true,
"--features": true,
"-F": true,
"--path": true,
"--git": true,
"--branch": true,
"--tag": true,
"--rev": true,
"--registry": true,
"--index": true,
"--root": true,
"--profile": true,
"--target": true,
"--bin": true,
"--example": true,
"-j": true,
"--jobs": true,
}

// Cargo returns a parser for `cargo` argv.
func Cargo() Parser { return cargoParser{} }

type cargoParser struct{}

func (cargoParser) Parse(args []string) ParseResult {
if len(args) == 0 {
return ParseResult{}
}
verb := args[0]
if verb != "add" && verb != "install" {
return ParseResult{}
}
rest := args[1:]

// A --vers/--version flag (if present) applies to the crate(s) named in
// the same command. Capture it so `cargo add serde --vers 1.0` pins.
var flagVersion string
for i, a := range rest {
switch {
case (a == "--vers" || a == "--version") && i+1 < len(rest):
flagVersion = rest[i+1]
case strings.HasPrefix(a, "--vers="):
flagVersion = strings.TrimPrefix(a, "--vers=")
case strings.HasPrefix(a, "--version="):
flagVersion = strings.TrimPrefix(a, "--version=")
}
}

positionals := splitPositionals(rest, cargoFlagsTakingArg)
pkgs := make([]PkgRef, 0, len(positionals))
for _, spec := range positionals {
// Local path installs aren't registry crates.
if strings.Contains(spec, "/") || strings.HasPrefix(spec, ".") {
continue
}
name, version := splitNameVersion(spec)
if version == "" {
version = flagVersion
}
if name == "" {
continue
}
pkgs = append(pkgs, PkgRef{Ecosystem: "crates.io", Name: name, Version: version})
}
if len(pkgs) == 0 {
return ParseResult{}
}
return ParseResult{IsInstall: true, Mode: ModeDirect, Packages: pkgs, Reason: "cargo " + verb}
}
75 changes: 75 additions & 0 deletions internal/parsers/cargo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package parsers

import (
"reflect"
"testing"
)

func TestCargoParser(t *testing.T) {
cases := []struct {
name string
args []string
want ParseResult
}{
{
name: "cargo add unpinned",
args: []string{"add", "serde"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "serde"}}, Reason: "cargo add"},
},
{
name: "cargo add @ version",
args: []string{"add", "serde@1.0.150"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "serde", Version: "1.0.150"}}, Reason: "cargo add"},
},
{
name: "cargo add --vers",
args: []string{"add", "serde", "--vers", "1.0.150"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "serde", Version: "1.0.150"}}, Reason: "cargo add"},
},
{
name: "cargo add --version=",
args: []string{"add", "serde", "--version=1.0.150"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "serde", Version: "1.0.150"}}, Reason: "cargo add"},
},
{
name: "cargo add with --features (flag value not treated as crate)",
args: []string{"add", "serde", "--features", "derive"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "serde"}}, Reason: "cargo add"},
},
{
name: "cargo install binary crate",
args: []string{"install", "ripgrep@14.1.0"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "crates.io", Name: "ripgrep", Version: "14.1.0"}}, Reason: "cargo install"},
},
{
name: "cargo install --path skips local",
args: []string{"install", "--path", "."},
want: ParseResult{},
},
{
name: "cargo build → passthrough",
args: []string{"build", "--release"},
want: ParseResult{},
},
{
name: "cargo test → passthrough",
args: []string{"test"},
want: ParseResult{},
},
{
name: "cargo bare → passthrough",
args: []string{},
want: ParseResult{},
},
}

p := Cargo()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := p.Parse(tc.args)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parse(%v):\n got: %+v\n want: %+v", tc.args, got, tc.want)
}
})
}
}
74 changes: 74 additions & 0 deletions internal/parsers/gem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package parsers

import "strings"

// Gem handles `gem install` (and its `gem i` alias).
// gem install rails → direct
// gem install rails -v 6.1.0 → direct, pinned (-v form)
// gem install rails --version 6.1.0 → direct, pinned
// gem install rails:6.1.0 → direct, pinned (colon form)
// gem update / list / build / ... → passthrough
//
// Local `.gem` file installs are skipped — the gate vets registry gems.

var gemFlagsTakingArg = map[string]bool{
"-v": true,
"--version": true,
"-i": true,
"--install-dir": true,
"-n": true,
"--bindir": true,
"--platform": true,
"-g": true,
"--file": true,
"-s": true,
"--source": true,
}

// Gem returns a parser for `gem` argv.
func Gem() Parser { return gemParser{} }

type gemParser struct{}

func (gemParser) Parse(args []string) ParseResult {
if len(args) == 0 || (args[0] != "install" && args[0] != "i") {
return ParseResult{}
}
rest := args[1:]

// -v/--version applies to the gem(s) named in the same command. RubyGems
// version specs can be operators (`-v '>= 6.0'`); we keep just the digits
// and let the server resolve. Trim common operator/space noise.
var flagVersion string
for i, a := range rest {
switch {
case (a == "-v" || a == "--version") && i+1 < len(rest):
flagVersion = strings.TrimLeft(rest[i+1], "=<>~! ")
case strings.HasPrefix(a, "--version="):
flagVersion = strings.TrimLeft(strings.TrimPrefix(a, "--version="), "=<>~! ")
}
}

positionals := splitPositionals(rest, gemFlagsTakingArg)
pkgs := make([]PkgRef, 0, len(positionals))
for _, spec := range positionals {
if strings.HasSuffix(spec, ".gem") || strings.Contains(spec, "/") {
continue
}
name := spec
version := flagVersion
// Colon form: `gem install rails:6.1.0`.
if i := strings.IndexByte(spec, ':'); i != -1 {
name = spec[:i]
version = spec[i+1:]
}
if name == "" {
continue
}
pkgs = append(pkgs, PkgRef{Ecosystem: "RubyGems", Name: name, Version: version})
}
if len(pkgs) == 0 {
return ParseResult{}
}
return ParseResult{IsInstall: true, Mode: ModeDirect, Packages: pkgs, Reason: "gem install"}
}
70 changes: 70 additions & 0 deletions internal/parsers/gem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package parsers

import (
"reflect"
"testing"
)

func TestGemParser(t *testing.T) {
cases := []struct {
name string
args []string
want ParseResult
}{
{
name: "gem install unpinned",
args: []string{"install", "rails"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rails"}}, Reason: "gem install"},
},
{
name: "gem install -v",
args: []string{"install", "rails", "-v", "6.1.0"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rails", Version: "6.1.0"}}, Reason: "gem install"},
},
{
name: "gem install --version=",
args: []string{"install", "rails", "--version=6.1.0"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rails", Version: "6.1.0"}}, Reason: "gem install"},
},
{
name: "gem install colon form",
args: []string{"install", "rails:6.1.0"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rails", Version: "6.1.0"}}, Reason: "gem install"},
},
{
name: "gem i alias",
args: []string{"i", "rack"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rack"}}, Reason: "gem install"},
},
{
name: "gem install operator version trimmed",
args: []string{"install", "rails", "-v", ">=6.0"},
want: ParseResult{IsInstall: true, Mode: ModeDirect, Packages: []PkgRef{{Ecosystem: "RubyGems", Name: "rails", Version: "6.0"}}, Reason: "gem install"},
},
{
name: "gem install local .gem skipped",
args: []string{"install", "./mygem-1.0.gem"},
want: ParseResult{},
},
{
name: "gem update → passthrough",
args: []string{"update"},
want: ParseResult{},
},
{
name: "gem list → passthrough",
args: []string{"list"},
want: ParseResult{},
},
}

p := Gem()
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := p.Parse(tc.args)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parse(%v):\n got: %+v\n want: %+v", tc.args, got, tc.want)
}
})
}
}
57 changes: 57 additions & 0 deletions internal/parsers/gomod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package parsers

import "strings"

// Go handles `go get` and `go install` for module-version specs.
// go get github.com/foo/bar@v1.2.3 → direct, pinned
// go install github.com/foo/cmd@latest → direct (latest → unpinned)
// go get golang.org/x/text → direct, unpinned
// go get ./... / go build / test ... → passthrough
//
// Go's version syntax is always module@version. `@latest`, `@upgrade`,
// `@patch`, `@none` are pseudo-selectors — we treat them as unpinned so the
// gate resolves the concrete latest.

// Go returns a parser for `go` argv.
func Go() Parser { return goParser{} }

type goParser struct{}

func (goParser) Parse(args []string) ParseResult {
if len(args) == 0 {
return ParseResult{}
}
verb := args[0]
if verb != "get" && verb != "install" {
return ParseResult{}
}

positionals := splitPositionals(args[1:], nil)
pkgs := make([]PkgRef, 0, len(positionals))
for _, spec := range positionals {
// Local paths and package-pattern meta-args aren't registry installs.
if spec == "all" || strings.HasPrefix(spec, ".") || strings.HasPrefix(spec, "/") ||
strings.Contains(spec, "...") {
continue
}
name := spec
version := ""
// Module paths never contain '@'; the last '@' separates the version.
if at := strings.LastIndexByte(spec, '@'); at != -1 {
name = spec[:at]
version = spec[at+1:]
switch version {
case "latest", "upgrade", "patch", "none":
version = ""
}
}
if name == "" {
continue
}
pkgs = append(pkgs, PkgRef{Ecosystem: "Go", Name: name, Version: version})
}
if len(pkgs) == 0 {
return ParseResult{}
}
return ParseResult{IsInstall: true, Mode: ModeDirect, Packages: pkgs, Reason: "go " + verb}
}
Loading
Loading