Skip to content

Commit

Permalink
Merge pull request #16460 from github/mbg/go/semver-type
Browse files Browse the repository at this point in the history
Go: Use new type for all semantic versions
  • Loading branch information
mbg committed Jun 7, 2024
2 parents 0ab67d1 + 9d1c2c6 commit ea3a3db
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 180 deletions.
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

// 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
Loading

0 comments on commit ea3a3db

Please sign in to comment.