github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/analyzers/golang/revision.go (about)

     1  package golang
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/apex/log"
     7  
     8  	"github.com/fossas/fossa-cli/analyzers/golang/resolver"
     9  	"github.com/fossas/fossa-cli/buildtools"
    10  	"github.com/fossas/fossa-cli/buildtools/gocmd"
    11  	"github.com/fossas/fossa-cli/pkg"
    12  )
    13  
    14  // Revision resolves a revision, returning buildtools.ErrNoRevisionForPackage
    15  // when no revision is found (unless a revision is not required).
    16  func (a *Analyzer) Revision(project Project, r resolver.Resolver, gopkg gocmd.Package) (pkg.Import, error) {
    17  	if a.Options.AllowNestedVendor ||
    18  		a.Options.AllowDeepVendor ||
    19  		a.Options.AllowExternalVendor ||
    20  		a.Options.AllowExternalVendorPrefix != "" {
    21  		return a.RevisionContextual(project, gopkg)
    22  	}
    23  	return a.RevisionDirect(project, r, gopkg)
    24  }
    25  
    26  // RevisionDirect resolves a revision by looking up its revision in the
    27  // project's lockfile. This works for most conventional projects.
    28  func (a *Analyzer) RevisionDirect(project Project, r resolver.Resolver, gopkg gocmd.Package) (pkg.Import, error) {
    29  	log.Debugf("Project: %#v", project)
    30  	log.Debugf("Package: %#v", gopkg)
    31  
    32  	name := Unvendor(gopkg.ImportPath)
    33  	revision, err := r.Resolve(name)
    34  	if err == buildtools.ErrNoRevisionForPackage {
    35  		log.Debugf("Could not find revision for package %#v", name)
    36  
    37  		if a.UnresolvedOK(project, gopkg) {
    38  			log.Debugf("Skipping package: %#v", gopkg)
    39  			return UnresolvedImport(gopkg), nil
    40  		}
    41  	}
    42  
    43  	if err == buildtools.ErrPackageIsIgnored {
    44  		return UnresolvedImport(gopkg), nil
    45  	}
    46  
    47  	if err != nil {
    48  		return pkg.Import{}, err
    49  	}
    50  	return revision, nil
    51  }
    52  
    53  // RevisionContextual resolve a revision by looking up its revision in the
    54  // lockfile of a vendoring parent. This supports complex use cases, such as
    55  // multi-project vendoring (Docker), and nested vendor folders (Hashicorp):
    56  //
    57  //   1. Multi-project example: docker/docker-ce does not vendor all of its
    58  //      dependencies within the project. Instead, it uses unvendored imports of
    59  //      docker/docker, which vendors imports of its own set of dependencies.
    60  //      This means that we need to do a lookup in docker/docker's lockfile to
    61  //      resolve a transitive dependency of docker/docker-ce.
    62  //   2. Nested vendor folder example: hashicorp/consul has nested vendor
    63  //      folders. This means that resolving a transitive dependency may require
    64  //      looking at the lockfile of the dependency that vendors it, or looking
    65  //      further up at any of the ancestors in the vendoring chain.
    66  //
    67  // In theory, this is only used for setups that _build very carefully_, but are
    68  // otherwise prone to exploding. In practice, this seems to be required a lot.
    69  func (a *Analyzer) RevisionContextual(project Project, gopkg gocmd.Package) (pkg.Import, error) {
    70  	log.Debugf("Project: %#v", project)
    71  	log.Debugf("Package: %#v", gopkg)
    72  
    73  	// Search upwards in parent directories.
    74  	for dir := VendorParent(gopkg.Dir); dir != "." && a.ParentOK(project, gopkg, dir); dir = VendorParent(dir) {
    75  		log.Debugf("Trying dir: %#v", dir)
    76  
    77  		revision, err := a.RevisionContextualLookup(project, gopkg, dir)
    78  		if err == ErrNoLockfileInDir {
    79  			log.Debugf("No lockfile found.")
    80  			if a.Options.AllowDeepVendor {
    81  				continue
    82  			} else {
    83  				break
    84  			}
    85  		}
    86  		if err != nil {
    87  			return pkg.Import{}, err
    88  		}
    89  
    90  		return revision, nil
    91  	}
    92  
    93  	if a.UnresolvedOK(project, gopkg) {
    94  		return UnresolvedImport(gopkg), nil
    95  	}
    96  	return pkg.Import{}, buildtools.ErrNoRevisionForPackage
    97  }
    98  
    99  // RevisionContextualLookup attempts to resolve a revision of a package using
   100  // the lockfile in a specific directory.
   101  func (a *Analyzer) RevisionContextualLookup(project Project, gopkg gocmd.Package, dir string) (pkg.Import, error) {
   102  	log.Debugf("Trying dir: %#v", dir)
   103  
   104  	// Try to get a resolver from a lockfile.
   105  	tool, err := LockfileIn(dir)
   106  	if err != nil {
   107  		return pkg.Import{}, err
   108  	}
   109  	r, err := a.ResolverFromLockfile(tool, dir)
   110  	if err != nil {
   111  		return pkg.Import{}, err
   112  	}
   113  
   114  	// Try to resolve the revision.
   115  	revision, err := a.RevisionDirect(project, r, gopkg)
   116  	if err != nil {
   117  		return pkg.Import{}, err
   118  	}
   119  	return revision, nil
   120  }
   121  
   122  // UnresolvedOK returns true if failing to resolve the revision of a package
   123  // is a non-fatal (rather than fatal) error.
   124  //
   125  // Packages require a resolved revision unless any of the following are true:
   126  //
   127  //   1. The package is part of the standard library.
   128  //   2. The package is internal.
   129  //   3. The package is within the project folder, but not under the vendor
   130  //      folder.
   131  //   4. A package at the project's folder would have an import path that is a
   132  //      prefix of the package path.
   133  //   5. Options.AllowUnresolved is true.
   134  //   6. Options.AllowUnresolvedPrefix is set one of its paths is a prefix of the
   135  //      package's import path.
   136  //
   137  // These exceptions are generally either "part of" (i.e. versioned as a unit
   138  // with) the project, or otherwise don't have revisions.
   139  func (a *Analyzer) UnresolvedOK(project Project, gopkg gocmd.Package) bool {
   140  	withinDir :=
   141  		strings.HasPrefix(gopkg.Dir, project.Dir) && !strings.Contains(gopkg.Dir, "/vendor/")
   142  
   143  	allowedPrefixes := strings.Split(a.Options.AllowUnresolvedPrefix, " ")
   144  	hasAllowedPrefix := false
   145  	for _, prefix := range allowedPrefixes {
   146  		if strings.HasPrefix(Unvendor(gopkg.ImportPath), prefix) {
   147  			hasAllowedPrefix = true
   148  		}
   149  	}
   150  	withinAllowedPrefix := (a.Options.AllowUnresolvedPrefix != "" && hasAllowedPrefix)
   151  
   152  	return gopkg.IsStdLib ||
   153  		gopkg.IsInternal ||
   154  		withinDir ||
   155  		strings.HasPrefix(Unvendor(gopkg.ImportPath), project.ImportPath) ||
   156  		a.Options.AllowUnresolved ||
   157  		withinAllowedPrefix
   158  }
   159  
   160  // ParentOK returns true if looking up the lockfile of gopkg in dir is allowed.
   161  //
   162  // A package may be looked up in a directory if:
   163  //
   164  //   1. The directory is within the main project.
   165  //   2. The directory is not within the main project, but we allow external
   166  //      project vendoring.
   167  //   3. The directory is not within the main project, but an allowed external
   168  //      vendoring import path prefix is a prefix of the import path that a
   169  //      package at the directory would have.
   170  func (a *Analyzer) ParentOK(project Project, gopkg gocmd.Package, dir string) bool {
   171  	log.Debugf("Project: %#v", project)
   172  	log.Debugf("Package: %#v", gopkg)
   173  	log.Debugf("Dir: %#v", dir)
   174  
   175  	// If this returns an error, it should already have failed when creating the
   176  	// project.
   177  	dirImportPath, err := ImportPath(dir)
   178  	if err != nil {
   179  		panic(err)
   180  	}
   181  	allowedPrefixes := strings.Split(a.Options.AllowExternalVendorPrefix, " ")
   182  	hasAllowedPrefix := false
   183  	for _, prefix := range allowedPrefixes {
   184  		if strings.HasPrefix(dirImportPath, prefix) {
   185  			hasAllowedPrefix = true
   186  		}
   187  	}
   188  	withinAllowedPrefix := (a.Options.AllowExternalVendorPrefix != "" && hasAllowedPrefix)
   189  
   190  	return strings.HasPrefix(dir, project.Dir) || a.Options.AllowExternalVendor || withinAllowedPrefix
   191  }