Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: Use new type for all semantic versions #16460

Merged
merged 15 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion go/extractor/autobuilder/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

162 changes: 79 additions & 83 deletions go/extractor/autobuilder/build-environment.go

Large diffs are not rendered by default.

66 changes: 37 additions & 29 deletions go/extractor/autobuilder/build-environment_test.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,55 @@
package autobuilder

import "testing"
import (
"testing"

"github.com/github/codeql-go/extractor/util"
)

func TestGetVersionToInstall(t *testing.T) {
tests := map[versionInfo]string{
type inputVersions struct {
modVersion string
envVersion string
}
tests := map[inputVersions]string{
// getVersionWhenGoModVersionNotFound()
{"", false, "", false}: maxGoVersion,
{"", false, "1.2.2", true}: maxGoVersion,
{"", false, "9999.0.1", true}: maxGoVersion,
{"", false, "1.11.13", true}: "",
{"", false, "1.20.3", true}: "",
{"", ""}: maxGoVersion.String(),
{"", "1.2.2"}: maxGoVersion.String(),
{"", "9999.0.1"}: maxGoVersion.String(),
{"", "1.11.13"}: "",
{"", "1.20.3"}: "",

// getVersionWhenGoModVersionTooHigh()
{"9999.0", true, "", false}: maxGoVersion,
{"9999.0", true, "9999.0.1", true}: "",
{"9999.0", true, "1.1", true}: maxGoVersion,
{"9999.0", true, minGoVersion, false}: maxGoVersion,
{"9999.0", true, maxGoVersion, true}: "",
{"9999.0", ""}: maxGoVersion.String(),
{"9999.0", "9999.0.1"}: "",
{"9999.0", "1.1"}: maxGoVersion.String(),
{"9999.0", minGoVersion.String()}: maxGoVersion.String(),
{"9999.0", maxGoVersion.String()}: "",

// getVersionWhenGoModVersionTooLow()
{"0.0", true, "", false}: minGoVersion,
{"0.0", true, "9999.0", true}: minGoVersion,
{"0.0", true, "1.2.2", true}: minGoVersion,
{"0.0", true, "1.20.3", true}: "",
{"0.0", ""}: minGoVersion.String(),
{"0.0", "9999.0"}: minGoVersion.String(),
{"0.0", "1.2.2"}: minGoVersion.String(),
{"0.0", "1.20.3"}: "",

// getVersionWhenGoModVersionSupported()
{"1.20", true, "", false}: "1.20",
{"1.11", true, "", false}: "1.11",
{"1.20", true, "1.2.2", true}: "1.20",
{"1.11", true, "1.2.2", true}: "1.11",
{"1.20", true, "9999.0.1", true}: "1.20",
{"1.11", true, "9999.0.1", true}: "1.11",
{"1.20", ""}: "1.20",
{"1.11", ""}: "1.11",
{"1.20", "1.2.2"}: "1.20",
{"1.11", "1.2.2"}: "1.11",
{"1.20", "9999.0.1"}: "1.20",
{"1.11", "9999.0.1"}: "1.11",
// go.mod version > go installation version
{"1.20", true, "1.11.13", true}: "1.20",
{"1.20", true, "1.12", true}: "1.20",
{"1.20", "1.11.13"}: "1.20",
{"1.20", "1.12"}: "1.20",
// go.mod version <= go installation version (Note comparisons ignore the patch version)
{"1.11", true, "1.20", true}: "",
{"1.11", true, "1.20.3", true}: "",
{"1.20", true, "1.20.3", true}: "",
{"1.11", "1.20"}: "",
{"1.11", "1.20.3"}: "",
{"1.20", "1.20.3"}: "",
}
for input, expected := range tests {
_, actual := getVersionToInstall(input)
if actual != expected {
_, actual := getVersionToInstall(versionInfo{util.NewSemVer(input.modVersion), util.NewSemVer(input.envVersion)})
if actual != util.NewSemVer(expected) {
t.Errorf("Expected getVersionToInstall(\"%s\") to be \"%s\", but got \"%s\".", input, expected, actual)
}
}
Expand Down
1 change: 0 additions & 1 deletion go/extractor/cli/go-autobuilder/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions go/extractor/cli/go-autobuilder/go-autobuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import (
"runtime"
"strings"

"golang.org/x/mod/semver"

"github.com/github/codeql-go/extractor/autobuilder"
"github.com/github/codeql-go/extractor/diagnostics"
"github.com/github/codeql-go/extractor/project"
Expand Down Expand Up @@ -156,7 +154,7 @@ func getNeedGopath(workspace project.GoWorkspace, importpath string) bool {
// Try to update `go.mod` and `go.sum` if the go version is >= 1.16.
func tryUpdateGoModAndGoSum(workspace project.GoWorkspace) {
// Go 1.16 and later won't automatically attempt to update go.mod / go.sum during package loading, so try to update them here:
if workspace.ModMode != project.ModVendor && workspace.DepMode == project.GoGetWithModules && semver.Compare(toolchain.GetEnvGoSemVer(), "v1.16") >= 0 {
if workspace.ModMode != project.ModVendor && workspace.DepMode == project.GoGetWithModules && toolchain.GetEnvGoSemVer().IsAtLeast(toolchain.V1_16) {
for _, goMod := range workspace.Modules {
// stat go.mod and go.sum
goModPath := goMod.Path
Expand Down Expand Up @@ -542,12 +540,12 @@ func installDependenciesAndBuild() {

// This diagnostic is not required if the system Go version is 1.21 or greater, since the
// Go tooling should install required Go versions as needed.
if semver.Compare(toolchain.GetEnvGoSemVer(), "v1.21.0") < 0 && greatestGoVersion.Found && semver.Compare("v"+greatestGoVersion.Version, toolchain.GetEnvGoSemVer()) > 0 {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer(), "v"+greatestGoVersion.Version)
if toolchain.GetEnvGoSemVer().IsOlderThan(toolchain.V1_21) && greatestGoVersion != nil && greatestGoVersion.IsNewerThan(toolchain.GetEnvGoSemVer()) {
diagnostics.EmitNewerGoVersionNeeded(toolchain.GetEnvGoSemVer().String(), greatestGoVersion.String())
if val, _ := os.LookupEnv("GITHUB_ACTIONS"); val == "true" {
log.Printf(
"A go.mod file requires version %s of Go, but version %s is installed. Consider adding an actions/setup-go step to your workflow.\n",
"v"+greatestGoVersion.Version,
greatestGoVersion,
toolchain.GetEnvGoSemVer())
}
}
Expand All @@ -559,7 +557,7 @@ func installDependenciesAndBuild() {
for i, workspace := range workspaces {
goVersionInfo := workspace.RequiredGoVersion()

fixGoVendorIssues(&workspace, goVersionInfo.Found)
fixGoVendorIssues(&workspace, goVersionInfo != nil)

tryUpdateGoModAndGoSum(workspace)

Expand Down
1 change: 0 additions & 1 deletion go/extractor/project/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 20 additions & 30 deletions go/extractor/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/github/codeql-go/extractor/toolchain"
"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)

// DependencyInstallerMode is an enum describing how dependencies should be installed
Expand Down Expand Up @@ -49,53 +48,47 @@ type GoWorkspace struct {
}

// Represents a nullable version string.
type GoVersionInfo struct {
// The version string, if any
Version string
// A value indicating whether a version string was found
Found bool
}
type GoVersionInfo = util.SemVer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're now combining what used to be two different states: "found a go version, but it was invalid" and "didn't find a go version". Is it okay that we can't distinguish those any more?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I checked to see if there were any cases where we cared about that distinction, but I'll check again to make sure.


// Determines the version of Go that is required by this workspace. This is, in order of preference:
// 1. The Go version specified in the `go.work` file, if any.
// 2. The greatest Go version specified in any `go.mod` file, if any.
func (workspace *GoWorkspace) RequiredGoVersion() GoVersionInfo {
func (workspace *GoWorkspace) RequiredGoVersion() util.SemVer {
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
// If we have parsed a `go.work` file, return the version number from it.
return GoVersionInfo{Version: workspace.WorkspaceFile.Go.Version, Found: true}
return util.NewSemVer(workspace.WorkspaceFile.Go.Version)
} else if workspace.Modules != nil && len(workspace.Modules) > 0 {
// Otherwise, if we have `go.work` files, find the greatest Go version in those.
var greatestVersion string = ""
var greatestVersion util.SemVer = nil
for _, module := range workspace.Modules {
if module.Module != nil && module.Module.Go != nil {
// If we have parsed the file, retrieve the version number we have already obtained.
if greatestVersion == "" || semver.Compare("v"+module.Module.Go.Version, "v"+greatestVersion) > 0 {
greatestVersion = module.Module.Go.Version
modVersion := util.NewSemVer(module.Module.Go.Version)
if greatestVersion == nil || modVersion.IsNewerThan(greatestVersion) {
greatestVersion = modVersion
}
} else {
modVersion := tryReadGoDirective(module.Path)
if modVersion.Found && (greatestVersion == "" || semver.Compare("v"+modVersion.Version, "v"+greatestVersion) > 0) {
greatestVersion = modVersion.Version
if modVersion != nil && (greatestVersion == nil || modVersion.IsNewerThan(greatestVersion)) {
greatestVersion = modVersion
}
}
}

// If we have found some version, return it.
if greatestVersion != "" {
return GoVersionInfo{Version: greatestVersion, Found: true}
}
return greatestVersion
}

return GoVersionInfo{Version: "", Found: false}
return nil
}

// Finds the greatest Go version required by any of the given `workspaces`.
// Returns a `GoVersionInfo` value with `Found: false` if no version information is available.
func RequiredGoVersion(workspaces *[]GoWorkspace) GoVersionInfo {
greatestGoVersion := GoVersionInfo{Version: "", Found: false}
func RequiredGoVersion(workspaces *[]GoWorkspace) util.SemVer {
var greatestGoVersion util.SemVer = nil
for _, workspace := range *workspaces {
goVersionInfo := workspace.RequiredGoVersion()
if goVersionInfo.Found && (!greatestGoVersion.Found || semver.Compare("v"+goVersionInfo.Version, "v"+greatestGoVersion.Version) > 0) {
if goVersionInfo != nil && (greatestGoVersion == nil || goVersionInfo.IsNewerThan(greatestGoVersion)) {
greatestGoVersion = goVersionInfo
}
}
Expand Down Expand Up @@ -183,7 +176,7 @@ var toolchainVersionRe *regexp.Regexp = regexp.MustCompile(`(?m)^([0-9]+\.[0-9]+
// there is no `toolchain` directive, and the Go language version is not a valid toolchain version.
func hasInvalidToolchainVersion(modFile *modfile.File) bool {
return modFile.Toolchain == nil && modFile.Go != nil &&
!toolchainVersionRe.Match([]byte(modFile.Go.Version)) && semver.Compare("v"+modFile.Go.Version, "v1.21.0") >= 0
!toolchainVersionRe.Match([]byte(modFile.Go.Version)) && util.NewSemVer(modFile.Go.Version).IsAtLeast(toolchain.V1_21)
}

// Given a list of `go.mod` file paths, try to parse them all. The resulting array of `GoModule` objects
Expand Down Expand Up @@ -537,17 +530,14 @@ const (

// argsForGoVersion returns the arguments to pass to the Go compiler for the given `ModMode` and
// Go version
func (m ModMode) ArgsForGoVersion(version string) []string {
func (m ModMode) ArgsForGoVersion(version util.SemVer) []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 {
if version.IsOlderThan(toolchain.V1_14) {
return []string{} // -mod=mod is the default behaviour for go <= 1.13, and is not accepted as an argument
} else {
return []string{"-mod=mod"}
Expand All @@ -574,7 +564,7 @@ func getModMode(depMode DependencyInstallerMode, baseDir string) ModMode {

// Tries to open `go.mod` and read a go directive, returning the version and whether it was found.
// The version string is returned in the "1.2.3" format.
func tryReadGoDirective(path string) GoVersionInfo {
func tryReadGoDirective(path string) util.SemVer {
versionRe := regexp.MustCompile(`(?m)^go[ \t\r]+([0-9]+\.[0-9]+(\.[0-9]+)?)`)
goMod, err := os.ReadFile(path)
if err != nil {
Expand All @@ -583,9 +573,9 @@ func tryReadGoDirective(path string) GoVersionInfo {
matches := versionRe.FindSubmatch(goMod)
if matches != nil {
if len(matches) > 1 {
return GoVersionInfo{string(matches[1]), true}
return util.NewSemVer(string(matches[1]))
}
}
}
return GoVersionInfo{"", false}
return nil
}
6 changes: 2 additions & 4 deletions go/extractor/toolchain/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 16 additions & 20 deletions go/extractor/toolchain/toolchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ import (
"strings"

"github.com/github/codeql-go/extractor/util"
"golang.org/x/mod/semver"
)

var V1_14 = util.NewSemVer("v1.14.0")
var V1_16 = util.NewSemVer("v1.16.0")
var V1_18 = util.NewSemVer("v1.18.0")
var V1_21 = util.NewSemVer("v1.21.0")

// Check if Go is installed in the environment.
func IsInstalled() bool {
_, err := exec.LookPath("go")
Expand All @@ -23,11 +27,11 @@ func IsInstalled() bool {
// The default Go version that is available on a system and a set of all versions
// that we know are installed on the system.
var goVersion = ""
var goVersions = map[string]struct{}{}
var goVersions = map[util.SemVer]struct{}{}

// Adds an entry to the set of installed Go versions for the normalised `version` number.
func addGoVersion(version string) {
goVersions[semver.Canonical("v"+version)] = struct{}{}
func addGoVersion(version util.SemVer) {
goVersions[version] = struct{}{}
}

// Returns the current Go version as returned by 'go version', e.g. go1.14.4
Expand All @@ -53,19 +57,19 @@ func GetEnvGoVersion() string {
}

goVersion = parseGoVersion(string(out))
addGoVersion(goVersion[2:])
addGoVersion(util.NewSemVer(goVersion))
}
return goVersion
}

// Determines whether, to our knowledge, `version` is available on the current system.
func HasGoVersion(version string) bool {
_, found := goVersions[semver.Canonical("v"+version)]
func HasGoVersion(version util.SemVer) bool {
_, found := goVersions[version]
return found
}

// Attempts to install the Go toolchain `version`.
func InstallVersion(workingDir string, version string) bool {
func InstallVersion(workingDir string, version util.SemVer) bool {
// No need to install it if we know that it is already installed.
if HasGoVersion(version) {
return true
Expand All @@ -74,7 +78,7 @@ func InstallVersion(workingDir string, version string) bool {
// Construct a command to invoke `go version` with `GOTOOLCHAIN=go1.N.0` to give
// Go a valid toolchain version to download the toolchain we need; subsequent commands
// should then work even with an invalid version that's still in `go.mod`
toolchainArg := "GOTOOLCHAIN=go" + semver.Canonical("v" + version)[1:]
toolchainArg := "GOTOOLCHAIN=go" + version.String()[1:]
versionCmd := Version()
versionCmd.Dir = workingDir
versionCmd.Env = append(os.Environ(), toolchainArg)
Expand Down Expand Up @@ -107,20 +111,12 @@ func InstallVersion(workingDir string, version string) bool {
}

// Returns the current Go version in semver format, e.g. v1.14.4
func GetEnvGoSemVer() string {
func GetEnvGoSemVer() util.SemVer {
goVersion := GetEnvGoVersion()
if !strings.HasPrefix(goVersion, "go") {
log.Fatalf("Expected 'go version' output of the form 'go1.2.3'; got '%s'", goVersion)
}
// Go versions don't follow the SemVer format, but the only exception we normally care about
// is release candidates; so this is a horrible hack to convert e.g. `go1.22rc1` into `go1.22-rc1`
// which is compatible with the SemVer specification
rcIndex := strings.Index(goVersion, "rc")
if rcIndex != -1 {
return semver.Canonical("v"+goVersion[2:rcIndex]) + "-" + goVersion[rcIndex:]
} else {
return semver.Canonical("v" + goVersion[2:])
}
return util.NewSemVer(goVersion)
}

// The 'go version' command may output warnings on separate lines before
Expand All @@ -137,7 +133,7 @@ func parseGoVersion(data string) string {

// Returns a value indicating whether the system Go toolchain supports workspaces.
func SupportsWorkspaces() bool {
return semver.Compare(GetEnvGoSemVer(), "v1.18.0") >= 0
return GetEnvGoSemVer().IsAtLeast(V1_18)
}

// Run `go mod tidy -e` in the directory given by `path`.
Expand Down