mirror of
https://github.com/github/codeql.git
synced 2025-12-18 18:10:39 +01:00
234 lines
6.1 KiB
Go
234 lines
6.1 KiB
Go
package project
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/github/codeql-go/extractor/diagnostics"
|
|
"github.com/github/codeql-go/extractor/util"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
func getDirs(paths []string) []string {
|
|
dirs := make([]string, len(paths))
|
|
for i, path := range paths {
|
|
dirs[i] = filepath.Dir(path)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
func checkDirsNested(inputDirs []string) (string, bool) {
|
|
// replace "." with "" so that we can check if all the paths are nested
|
|
dirs := make([]string, len(inputDirs))
|
|
for i, inputDir := range inputDirs {
|
|
if inputDir == "." {
|
|
dirs[i] = ""
|
|
} else {
|
|
dirs[i] = inputDir
|
|
}
|
|
}
|
|
// the paths were generated by a depth-first search so I think they might
|
|
// be sorted, but we sort them just in case
|
|
sort.Strings(dirs)
|
|
for _, dir := range dirs {
|
|
if !strings.HasPrefix(dir, dirs[0]) {
|
|
return "", false
|
|
}
|
|
}
|
|
return dirs[0], true
|
|
}
|
|
|
|
// Returns the directory to run the go build in and whether to use a go.mod
|
|
// file.
|
|
func findGoModFiles(emitDiagnostics bool) (baseDir string, useGoMod bool) {
|
|
goModPaths := util.FindAllFilesWithName(".", "go.mod", "vendor")
|
|
if len(goModPaths) == 0 {
|
|
baseDir = "."
|
|
useGoMod = false
|
|
return
|
|
}
|
|
goModDirs := getDirs(goModPaths)
|
|
if util.AnyGoFilesOutsideDirs(".", goModDirs...) {
|
|
if emitDiagnostics {
|
|
diagnostics.EmitGoFilesOutsideGoModules(goModPaths)
|
|
}
|
|
baseDir = "."
|
|
useGoMod = false
|
|
return
|
|
}
|
|
if len(goModPaths) > 1 {
|
|
// currently not supported
|
|
baseDir = "."
|
|
commonRoot, nested := checkDirsNested(goModDirs)
|
|
if nested && commonRoot == "" {
|
|
useGoMod = true
|
|
} else {
|
|
useGoMod = false
|
|
}
|
|
if emitDiagnostics {
|
|
if nested {
|
|
diagnostics.EmitMultipleGoModFoundNested(goModPaths)
|
|
} else {
|
|
diagnostics.EmitMultipleGoModFoundNotNested(goModPaths)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if emitDiagnostics {
|
|
if goModDirs[0] == "." {
|
|
diagnostics.EmitSingleRootGoModFound(goModPaths[0])
|
|
} else {
|
|
diagnostics.EmitSingleNonRootGoModFound(goModPaths[0])
|
|
}
|
|
}
|
|
baseDir = goModDirs[0]
|
|
useGoMod = true
|
|
return
|
|
}
|
|
|
|
// DependencyInstallerMode is an enum describing how dependencies should be installed
|
|
type DependencyInstallerMode int
|
|
|
|
const (
|
|
// GoGetNoModules represents dependency installation using `go get` without modules
|
|
GoGetNoModules DependencyInstallerMode = iota
|
|
// GoGetWithModules represents dependency installation using `go get` with modules
|
|
GoGetWithModules
|
|
// Dep represent dependency installation using `dep ensure`
|
|
Dep
|
|
// Glide represents dependency installation using `glide install`
|
|
Glide
|
|
)
|
|
|
|
// Returns the appropriate DependencyInstallerMode for the current project
|
|
func getDepMode(emitDiagnostics bool) (DependencyInstallerMode, string) {
|
|
bazelPaths := util.FindAllFilesWithName(".", "BUILD", "vendor")
|
|
bazelPaths = append(bazelPaths, util.FindAllFilesWithName(".", "BUILD.bazel", "vendor")...)
|
|
if len(bazelPaths) > 0 {
|
|
// currently not supported
|
|
if emitDiagnostics {
|
|
diagnostics.EmitBazelBuildFilesFound(bazelPaths)
|
|
}
|
|
}
|
|
|
|
goWorkPaths := util.FindAllFilesWithName(".", "go.work", "vendor")
|
|
if len(goWorkPaths) > 0 {
|
|
// currently not supported
|
|
if emitDiagnostics {
|
|
diagnostics.EmitGoWorkFound(goWorkPaths)
|
|
}
|
|
}
|
|
|
|
baseDir, useGoMod := findGoModFiles(emitDiagnostics)
|
|
if useGoMod {
|
|
log.Println("Found go.mod, enabling go modules")
|
|
return GoGetWithModules, baseDir
|
|
}
|
|
|
|
if util.FileExists("Gopkg.toml") {
|
|
if emitDiagnostics {
|
|
diagnostics.EmitGopkgTomlFound()
|
|
}
|
|
log.Println("Found Gopkg.toml, using dep instead of go get")
|
|
return Dep, "."
|
|
}
|
|
|
|
if util.FileExists("glide.yaml") {
|
|
if emitDiagnostics {
|
|
diagnostics.EmitGlideYamlFound()
|
|
}
|
|
log.Println("Found glide.yaml, using Glide instead of go get")
|
|
return Glide, "."
|
|
}
|
|
return GoGetNoModules, "."
|
|
}
|
|
|
|
// ModMode corresponds to the possible values of the -mod flag for the Go compiler
|
|
type ModMode int
|
|
|
|
const (
|
|
ModUnset ModMode = iota
|
|
ModReadonly
|
|
ModMod
|
|
ModVendor
|
|
)
|
|
|
|
// argsForGoVersion returns the arguments to pass to the Go compiler for the given `ModMode` and
|
|
// Go version
|
|
func (m ModMode) ArgsForGoVersion(version string) []string {
|
|
switch m {
|
|
case ModUnset:
|
|
return []string{}
|
|
case ModReadonly:
|
|
return []string{"-mod=readonly"}
|
|
case ModMod:
|
|
if !semver.IsValid(version) {
|
|
log.Fatalf("Invalid Go semver: '%s'", version)
|
|
}
|
|
if semver.Compare(version, "v1.14") < 0 {
|
|
return []string{} // -mod=mod is the default behaviour for go <= 1.13, and is not accepted as an argument
|
|
} else {
|
|
return []string{"-mod=mod"}
|
|
}
|
|
case ModVendor:
|
|
return []string{"-mod=vendor"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Returns the appropriate ModMode for the current project
|
|
func getModMode(depMode DependencyInstallerMode, baseDir string) ModMode {
|
|
if depMode == GoGetWithModules {
|
|
// if a vendor/modules.txt file exists, we assume that there are vendored Go dependencies, and
|
|
// skip the dependency installation step and run the extractor with `-mod=vendor`
|
|
if util.FileExists(filepath.Join(baseDir, "vendor", "modules.txt")) {
|
|
return ModVendor
|
|
} else if util.DirExists(filepath.Join(baseDir, "vendor")) {
|
|
return ModMod
|
|
}
|
|
}
|
|
return ModUnset
|
|
}
|
|
|
|
type BuildInfo struct {
|
|
DepMode DependencyInstallerMode
|
|
ModMode ModMode
|
|
BaseDir string
|
|
}
|
|
|
|
func GetBuildInfo(emitDiagnostics bool) BuildInfo {
|
|
depMode, baseDir := getDepMode(true)
|
|
modMode := getModMode(depMode, baseDir)
|
|
return BuildInfo{depMode, modMode, baseDir}
|
|
}
|
|
|
|
type GoVersionInfo struct {
|
|
// The version string, if any
|
|
Version string
|
|
// A value indicating whether a version string was found
|
|
Found bool
|
|
}
|
|
|
|
// Tries to open `go.mod` and read a go directive, returning the version and whether it was found.
|
|
func TryReadGoDirective(buildInfo BuildInfo) GoVersionInfo {
|
|
if buildInfo.DepMode == GoGetWithModules {
|
|
versionRe := regexp.MustCompile(`(?m)^go[ \t\r]+([0-9]+\.[0-9]+(\.[0-9]+)?)$`)
|
|
goMod, err := os.ReadFile(filepath.Join(buildInfo.BaseDir, "go.mod"))
|
|
if err != nil {
|
|
log.Println("Failed to read go.mod to check for missing Go version")
|
|
} else {
|
|
matches := versionRe.FindSubmatch(goMod)
|
|
if matches != nil {
|
|
if len(matches) > 1 {
|
|
return GoVersionInfo{string(matches[1]), true}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return GoVersionInfo{"", false}
|
|
}
|