Skip to content

Commit b9cddb0

Browse files
committed
feat(ruby): Add Ruby analysis
1 parent b06eb62 commit b9cddb0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+7301
-282
lines changed

.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.git
2+
docker

Gopkg.lock

+19-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

analyzers/analyzer.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/fossas/fossa-cli/analyzers/golang"
88
"github.com/fossas/fossa-cli/analyzers/nodejs"
99
"github.com/fossas/fossa-cli/analyzers/python"
10+
"github.com/fossas/fossa-cli/analyzers/ruby"
1011

1112
"github.com/fossas/fossa-cli/module"
1213
"github.com/fossas/fossa-cli/pkg"
@@ -54,7 +55,7 @@ func New(key pkg.Type, options map[string]interface{}) (Analyzer, error) {
5455
case pkg.Python:
5556
return python.New(options)
5657
case pkg.Ruby:
57-
return nil, ErrAnalyzerNotImplemented
58+
return ruby.New(options)
5859
case pkg.Scala:
5960
return nil, ErrAnalyzerNotImplemented
6061
case pkg.VendoredArchives:

analyzers/bower/bower.go

+15-10
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,28 @@ import (
1515
"github.com/fossas/fossa-cli/module"
1616
)
1717

18-
type Configuration struct {
18+
type ConfigFile struct {
1919
Cwd string `json:"cwd"`
2020
Directory string `json:"directory"`
2121
Registry string `json:"registry"`
2222
}
2323

24-
// BowerBuilder implements Builder for Bower
25-
type BowerBuilder struct {
24+
type Analyzer struct {
2625
NodeCmd string
2726
NodeVersion string
2827

2928
BowerCmd string
3029
BowerVersion string
30+
31+
Options Options
32+
}
33+
34+
type Options struct {
35+
ComponentsDir string `mapstructure:"components-dir"`
3136
}
3237

3338
// Initialize collects metadata on Node and Bower binaries
34-
func (builder *BowerBuilder) Initialize() error {
39+
func (builder *Analyzer) Initialize() error {
3540
log.Logger.Debug("Initializing Bower builder...")
3641

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

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

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

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

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

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

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

185190
// IsModule is not implemented
186-
func (builder *BowerBuilder) IsModule(target string) (bool, error) {
187-
return false, errors.New("IsModule is not implemented for BowerBuilder")
191+
func (builder *Analyzer) IsModule(target string) (bool, error) {
192+
return false, errors.New("IsModule is not implemented for Analyzer")
188193
}
189194

190195
// DiscoverModules finds any bower.json modules not in node_modules or bower_components folders
191-
func (builder *BowerBuilder) DiscoverModules(dir string) ([]module.Config, error) {
196+
func (builder *Analyzer) DiscoverModules(dir string) ([]module.Config, error) {
192197
var moduleConfigs []module.Config
193198
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
194199
if err != nil {

analyzers/golang/lockfile.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
// Errors that occur while finding lockfiles.
1414
var (
1515
ErrNoLockfileInDir = errors.New("could not find lockfile in directory")
16-
ErrNoNearestLockfile = errors.New("could not nearest lockfile of directory")
16+
ErrNoNearestLockfile = errors.New("could not find nearest lockfile of directory")
1717
)
1818

1919
// LockfileIn returns the type of lockfile within a directory, or

analyzers/golang/vcs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
// Errors that occur when finding VCS repositories.
1212
var (
1313
ErrNoVCSInDir = errors.New("could not find VCS repository in directory")
14-
ErrNoNearestVCS = errors.New("could not nearest VCS repository in directory")
14+
ErrNoNearestVCS = errors.New("could not find nearest VCS repository in directory")
1515
)
1616

1717
// VCSIn returns the type of VCS repository rooted at a directory, or

analyzers/nodejs/nodejs.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (a *Analyzer) Discover(dir string) ([]module.Module, error) {
109109
var modules []module.Module
110110
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
111111
if err != nil {
112-
log.Logger.Debugf("Failed to access path %s: %s\a", path, err.Error())
112+
log.Logger.Debugf("Failed to access path %s: %s\n", path, err.Error())
113113
return err
114114
}
115115

analyzers/python/python.go

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Package python provides analysers for Python projects.
2+
//
3+
// A `BuildTarget` in Python is the directory of the Python project, generally
4+
// containing `requirements.txt` or `setup.py`.
15
package python
26

37
import (
@@ -18,6 +22,7 @@ type Analyzer struct {
1822
PythonCmd string
1923
PythonVersion string
2024

25+
Pip pip.Pip
2126
Options Options
2227
}
2328

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

4146
// Construct analyzer.
42-
cmd, version, err := exec.Which("--version", os.Getenv("FOSSA_PYTHON_CMD"), "python", "python3", "python2.7")
47+
pythonCmd, pythonVersion, err := exec.Which("--version", os.Getenv("FOSSA_PYTHON_CMD"), "python", "python3", "python2.7")
4348
if err != nil {
4449
return nil, err
4550
}
51+
// TODO: this should be fatal depending on the configured strategy.
52+
pipCmd, _, err := exec.Which("--version", os.Getenv("FOSSA_PIP_CMD"), "pip3", "pip")
53+
if err != nil {
54+
log.Logger.Warningf("`pip` command not detected")
55+
}
4656
return &Analyzer{
47-
PythonCmd: cmd,
48-
PythonVersion: version,
49-
Options: options,
57+
PythonCmd: pythonCmd,
58+
PythonVersion: pythonVersion,
59+
60+
Pip: pip.Pip{
61+
Cmd: pipCmd,
62+
PythonCmd: pythonCmd,
63+
},
64+
Options: options,
5065
}, nil
5166
}
5267

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

106121
func (a *Analyzer) Build(m module.Module) error {
107-
p := pip.Pip{
108-
PythonCmd: a.PythonCmd,
109-
}
110-
return p.Install(a.reqFile(m))
122+
return a.Pip.Install(a.requirementsFile(m))
111123
}
112124

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

117129
func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
118-
p := pip.Pip{
119-
PythonCmd: a.PythonCmd,
120-
}
121130
switch a.Options.Strategy {
122131
case "deptree":
123-
tree, err := p.DepTree()
132+
tree, err := a.Pip.DepTree()
124133
if err != nil {
125134
return m, err
126135
}
@@ -129,7 +138,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
129138
m.Deps = graph
130139
return m, nil
131140
case "pip":
132-
reqs, err := p.List()
141+
reqs, err := a.Pip.List()
133142
if err != nil {
134143
return m, err
135144
}
@@ -138,7 +147,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
138147
case "requirements":
139148
fallthrough
140149
default:
141-
reqs, err := pip.FromFile(a.reqFile(m))
150+
reqs, err := pip.FromFile(a.requirementsFile(m))
142151
if err != nil {
143152
return m, err
144153
}
@@ -147,7 +156,7 @@ func (a *Analyzer) Analyze(m module.Module) (module.Module, error) {
147156
}
148157
}
149158

150-
func (a *Analyzer) reqFile(m module.Module) string {
159+
func (a *Analyzer) requirementsFile(m module.Module) string {
151160
reqFilename := filepath.Join(m.Dir, "requirements.txt")
152161
if a.Options.RequirementsPath != "" {
153162
reqFilename = a.Options.RequirementsPath

analyzers/ruby/bundler.go

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package ruby
2+
3+
import (
4+
"github.com/fossas/fossa-cli/buildtools/bundler"
5+
"github.com/fossas/fossa-cli/pkg"
6+
)
7+
8+
func FromGems(gems []bundler.Gem) ([]pkg.Import, map[pkg.ID]pkg.Package) {
9+
var imports []pkg.Import
10+
graph := make(map[pkg.ID]pkg.Package)
11+
for _, gem := range gems {
12+
id := pkg.ID{
13+
Type: pkg.Ruby,
14+
Name: gem.Name,
15+
Revision: gem.Revision,
16+
}
17+
imports = append(imports, pkg.Import{
18+
Resolved: id,
19+
})
20+
graph[id] = pkg.Package{
21+
ID: id,
22+
Strategy: "bundler-list",
23+
}
24+
}
25+
return imports, graph
26+
}
27+
28+
func FromLockfile(lockfile bundler.Lockfile) ([]pkg.Import, map[pkg.ID]pkg.Package) {
29+
// Construct a map of all dependencies.
30+
nameToID := make(map[string]pkg.ID)
31+
32+
AddSpecs(nameToID, lockfile.Git)
33+
AddSpecs(nameToID, lockfile.Path)
34+
AddSpecs(nameToID, lockfile.Gem)
35+
36+
// Build the dependency graph.
37+
graph := make(map[pkg.ID]pkg.Package)
38+
39+
AddToGraph(graph, nameToID, lockfile.Git)
40+
AddToGraph(graph, nameToID, lockfile.Path)
41+
AddToGraph(graph, nameToID, lockfile.Gem)
42+
43+
var imports []pkg.Import
44+
for _, dep := range lockfile.Dependencies {
45+
imports = append(imports, pkg.Import{
46+
Target: dep.String(),
47+
Resolved: nameToID[dep.Name],
48+
})
49+
}
50+
51+
return imports, graph
52+
}
53+
54+
func AddSpecs(lookup map[string]pkg.ID, sections []bundler.Section) {
55+
for _, section := range sections {
56+
for _, spec := range section.Specs {
57+
location := section.Remote
58+
if section.Type == "GIT" {
59+
location = section.Remote + "@" + section.Revision
60+
}
61+
lookup[spec.Name] = pkg.ID{
62+
Type: pkg.Ruby,
63+
Name: spec.Name,
64+
Revision: spec.Version,
65+
Location: location,
66+
}
67+
}
68+
}
69+
}
70+
71+
func AddToGraph(graph map[pkg.ID]pkg.Package, lookup map[string]pkg.ID, sections []bundler.Section) {
72+
for _, section := range sections {
73+
for _, spec := range section.Specs {
74+
var imports []pkg.Import
75+
for _, dep := range spec.Dependencies {
76+
imports = append(imports, pkg.Import{
77+
Target: dep.String(),
78+
Resolved: lookup[dep.Name],
79+
})
80+
}
81+
82+
id := lookup[spec.Name]
83+
graph[id] = pkg.Package{
84+
ID: id,
85+
Imports: imports,
86+
}
87+
}
88+
}
89+
}
90+
91+
func FilteredLockfile(gems []bundler.Gem, lockfile bundler.Lockfile) ([]pkg.Import, map[pkg.ID]pkg.Package) {
92+
// Construct set of allowed gems.
93+
gemSet := make(map[string]bool)
94+
for _, gem := range gems {
95+
gemSet[gem.Name] = true
96+
}
97+
98+
// Filter lockfile results.
99+
imports, graph := FromLockfile(lockfile)
100+
101+
var filteredImports []pkg.Import
102+
filteredGraph := make(map[pkg.ID]pkg.Package)
103+
104+
for _, dep := range imports {
105+
if _, ok := gemSet[dep.Resolved.Name]; ok {
106+
filteredImports = append(filteredImports, dep)
107+
}
108+
}
109+
110+
for id, pkg := range graph {
111+
if _, ok := gemSet[id.Name]; ok {
112+
filteredGraph[id] = pkg
113+
}
114+
}
115+
116+
return filteredImports, filteredGraph
117+
}

0 commit comments

Comments
 (0)