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 }