Files
codeql/go/extractor/project/project.go
2024-01-23 13:59:33 +00:00

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}
}