Skip to content

Commit

Permalink
feat(ruby): Add Ruby analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
elldritch committed Jun 24, 2018
1 parent b06eb62 commit b9cddb0
Show file tree
Hide file tree
Showing 48 changed files with 7,301 additions and 282 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.git
docker
20 changes: 19 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion analyzers/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/fossas/fossa-cli/analyzers/golang"
"github.com/fossas/fossa-cli/analyzers/nodejs"
"github.com/fossas/fossa-cli/analyzers/python"
"github.com/fossas/fossa-cli/analyzers/ruby"

"github.com/fossas/fossa-cli/module"
"github.com/fossas/fossa-cli/pkg"
Expand Down Expand Up @@ -54,7 +55,7 @@ func New(key pkg.Type, options map[string]interface{}) (Analyzer, error) {
case pkg.Python:
return python.New(options)
case pkg.Ruby:
return nil, ErrAnalyzerNotImplemented
return ruby.New(options)
case pkg.Scala:
return nil, ErrAnalyzerNotImplemented
case pkg.VendoredArchives:
Expand Down
25 changes: 15 additions & 10 deletions analyzers/bower/bower.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@ import (
"github.com/fossas/fossa-cli/module"
)

type Configuration struct {
type ConfigFile struct {
Cwd string `json:"cwd"`
Directory string `json:"directory"`
Registry string `json:"registry"`
}

// BowerBuilder implements Builder for Bower
type BowerBuilder struct {
type Analyzer struct {
NodeCmd string
NodeVersion string

BowerCmd string
BowerVersion string

Options Options
}

type Options struct {
ComponentsDir string `mapstructure:"components-dir"`
}

// Initialize collects metadata on Node and Bower binaries
func (builder *BowerBuilder) Initialize() error {
func (builder *Analyzer) Initialize() error {
log.Logger.Debug("Initializing Bower builder...")

// Set Node context variables
Expand All @@ -55,7 +60,7 @@ func (builder *BowerBuilder) Initialize() error {
}

// Build runs `bower install --production` and cleans with `rm -rf bower_components`
func (builder *BowerBuilder) Build(m module.Module, force bool) error {
func (builder *Analyzer) Build(m module.Module, force bool) error {
log.Logger.Debugf("Running Bower build: %#v", m, force)

if force {
Expand Down Expand Up @@ -114,7 +119,7 @@ func normalizeBowerComponents(parent module.ImportPath, c bowerListManifest) []b

// Analyze reads the output of `bower ls --json`
// TODO: fall back to old method of reading `bower_components/*/.bower.json`s?
func (builder *BowerBuilder) Analyze(m module.Module, allowUnresolved bool) ([]module.Dependency, error) {
func (builder *Analyzer) Analyze(m module.Module, allowUnresolved bool) ([]module.Dependency, error) {
log.Logger.Debugf("Running Bower analysis: %#v %#v", m, allowUnresolved)

stdout, _, err := exec.Run(exec.Cmd{Dir: m.Dir, Name: "bower", Argv: []string{"ls", "--json"}})
Expand Down Expand Up @@ -168,7 +173,7 @@ func resolveBowerComponentsDirectory(dir string) string {
}

// IsBuilt checks for the existence of `$PROJECT/bower_components`
func (builder *BowerBuilder) IsBuilt(m module.Module, allowUnresolved bool) (bool, error) {
func (builder *Analyzer) IsBuilt(m module.Module, allowUnresolved bool) (bool, error) {
log.Logger.Debug("Checking Bower build: %#v %#v", m, allowUnresolved)

// TODO: Check if the installed modules are consistent with what's in the
Expand All @@ -183,12 +188,12 @@ func (builder *BowerBuilder) IsBuilt(m module.Module, allowUnresolved bool) (boo
}

// IsModule is not implemented
func (builder *BowerBuilder) IsModule(target string) (bool, error) {
return false, errors.New("IsModule is not implemented for BowerBuilder")
func (builder *Analyzer) IsModule(target string) (bool, error) {
return false, errors.New("IsModule is not implemented for Analyzer")
}

// DiscoverModules finds any bower.json modules not in node_modules or bower_components folders
func (builder *BowerBuilder) DiscoverModules(dir string) ([]module.Config, error) {
func (builder *Analyzer) DiscoverModules(dir string) ([]module.Config, error) {
var moduleConfigs []module.Config
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion analyzers/golang/lockfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// Errors that occur while finding lockfiles.
var (
ErrNoLockfileInDir = errors.New("could not find lockfile in directory")
ErrNoNearestLockfile = errors.New("could not nearest lockfile of directory")
ErrNoNearestLockfile = errors.New("could not find nearest lockfile of directory")
)

// LockfileIn returns the type of lockfile within a directory, or
Expand Down
2 changes: 1 addition & 1 deletion analyzers/golang/vcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
// Errors that occur when finding VCS repositories.
var (
ErrNoVCSInDir = errors.New("could not find VCS repository in directory")
ErrNoNearestVCS = errors.New("could not nearest VCS repository in directory")
ErrNoNearestVCS = errors.New("could not find nearest VCS repository in directory")
)

// VCSIn returns the type of VCS repository rooted at a directory, or
Expand Down
2 changes: 1 addition & 1 deletion analyzers/nodejs/nodejs.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (a *Analyzer) Discover(dir string) ([]module.Module, error) {
var modules []module.Module
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Logger.Debugf("Failed to access path %s: %s\a", path, err.Error())
log.Logger.Debugf("Failed to access path %s: %s\n", path, err.Error())
return err
}

Expand Down
39 changes: 24 additions & 15 deletions analyzers/python/python.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Package python provides analysers for Python projects.
//
// A `BuildTarget` in Python is the directory of the Python project, generally
// containing `requirements.txt` or `setup.py`.
package python

import (
Expand All @@ -18,6 +22,7 @@ type Analyzer struct {
PythonCmd string
PythonVersion string

Pip pip.Pip
Options Options
}

Expand All @@ -39,14 +44,24 @@ func New(opts map[string]interface{}) (*Analyzer, error) {
log.Logger.Debug("Decoded options: %#v", options)

// Construct analyzer.
cmd, version, err := exec.Which("--version", os.Getenv("FOSSA_PYTHON_CMD"), "python", "python3", "python2.7")
pythonCmd, pythonVersion, err := exec.Which("--version", os.Getenv("FOSSA_PYTHON_CMD"), "python", "python3", "python2.7")
if err != nil {
return nil, err
}
// TODO: this should be fatal depending on the configured strategy.
pipCmd, _, err := exec.Which("--version", os.Getenv("FOSSA_PIP_CMD"), "pip3", "pip")
if err != nil {
log.Logger.Warningf("`pip` command not detected")
}
return &Analyzer{
PythonCmd: cmd,
PythonVersion: version,
Options: options,
PythonCmd: pythonCmd,
PythonVersion: pythonVersion,

Pip: pip.Pip{
Cmd: pipCmd,
PythonCmd: pythonCmd,
},
Options: options,
}, nil
}

Expand Down Expand Up @@ -104,23 +119,17 @@ func (a *Analyzer) Clean(m module.Module) error {
}

func (a *Analyzer) Build(m module.Module) error {
p := pip.Pip{
PythonCmd: a.PythonCmd,
}
return p.Install(a.reqFile(m))
return a.Pip.Install(a.requirementsFile(m))
}

func (a *Analyzer) IsBuilt(m module.Module) (bool, error) {
return true, nil
}

func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
p := pip.Pip{
PythonCmd: a.PythonCmd,
}
switch a.Options.Strategy {
case "deptree":
tree, err := p.DepTree()
tree, err := a.Pip.DepTree()
if err != nil {
return m, err
}
Expand All @@ -129,7 +138,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
m.Deps = graph
return m, nil
case "pip":
reqs, err := p.List()
reqs, err := a.Pip.List()
if err != nil {
return m, err
}
Expand All @@ -138,7 +147,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
case "requirements":
fallthrough
default:
reqs, err := pip.FromFile(a.reqFile(m))
reqs, err := pip.FromFile(a.requirementsFile(m))
if err != nil {
return m, err
}
Expand All @@ -147,7 +156,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
}
}

func (a *Analyzer) reqFile(m module.Module) string {
func (a *Analyzer) requirementsFile(m module.Module) string {
reqFilename := filepath.Join(m.Dir, "requirements.txt")
if a.Options.RequirementsPath != "" {
reqFilename = a.Options.RequirementsPath
Expand Down
117 changes: 117 additions & 0 deletions analyzers/ruby/bundler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package ruby

import (
"github.com/fossas/fossa-cli/buildtools/bundler"
"github.com/fossas/fossa-cli/pkg"
)

func FromGems(gems []bundler.Gem) ([]pkg.Import, map[pkg.ID]pkg.Package) {
var imports []pkg.Import
graph := make(map[pkg.ID]pkg.Package)
for _, gem := range gems {
id := pkg.ID{
Type: pkg.Ruby,
Name: gem.Name,
Revision: gem.Revision,
}
imports = append(imports, pkg.Import{
Resolved: id,
})
graph[id] = pkg.Package{
ID: id,
Strategy: "bundler-list",
}
}
return imports, graph
}

func FromLockfile(lockfile bundler.Lockfile) ([]pkg.Import, map[pkg.ID]pkg.Package) {
// Construct a map of all dependencies.
nameToID := make(map[string]pkg.ID)

AddSpecs(nameToID, lockfile.Git)
AddSpecs(nameToID, lockfile.Path)
AddSpecs(nameToID, lockfile.Gem)

// Build the dependency graph.
graph := make(map[pkg.ID]pkg.Package)

AddToGraph(graph, nameToID, lockfile.Git)
AddToGraph(graph, nameToID, lockfile.Path)
AddToGraph(graph, nameToID, lockfile.Gem)

var imports []pkg.Import
for _, dep := range lockfile.Dependencies {
imports = append(imports, pkg.Import{
Target: dep.String(),
Resolved: nameToID[dep.Name],
})
}

return imports, graph
}

func AddSpecs(lookup map[string]pkg.ID, sections []bundler.Section) {
for _, section := range sections {
for _, spec := range section.Specs {
location := section.Remote
if section.Type == "GIT" {
location = section.Remote + "@" + section.Revision
}
lookup[spec.Name] = pkg.ID{
Type: pkg.Ruby,
Name: spec.Name,
Revision: spec.Version,
Location: location,
}
}
}
}

func AddToGraph(graph map[pkg.ID]pkg.Package, lookup map[string]pkg.ID, sections []bundler.Section) {
for _, section := range sections {
for _, spec := range section.Specs {
var imports []pkg.Import
for _, dep := range spec.Dependencies {
imports = append(imports, pkg.Import{
Target: dep.String(),
Resolved: lookup[dep.Name],
})
}

id := lookup[spec.Name]
graph[id] = pkg.Package{
ID: id,
Imports: imports,
}
}
}
}

func FilteredLockfile(gems []bundler.Gem, lockfile bundler.Lockfile) ([]pkg.Import, map[pkg.ID]pkg.Package) {
// Construct set of allowed gems.
gemSet := make(map[string]bool)
for _, gem := range gems {
gemSet[gem.Name] = true
}

// Filter lockfile results.
imports, graph := FromLockfile(lockfile)

var filteredImports []pkg.Import
filteredGraph := make(map[pkg.ID]pkg.Package)

for _, dep := range imports {
if _, ok := gemSet[dep.Resolved.Name]; ok {
filteredImports = append(filteredImports, dep)
}
}

for id, pkg := range graph {
if _, ok := gemSet[id.Name]; ok {
filteredGraph[id] = pkg
}
}

return filteredImports, filteredGraph
}

0 comments on commit b9cddb0

Please sign in to comment.