Skip to content

Commit 317790e

Browse files
authored
Merge pull request github#16703 from github/mbg/go/improve-version-selection-v2
Go: Use toolchain directives for version selection if available, and add tests (v2)
2 parents ec34007 + d4adc37 commit 317790e

File tree

3 files changed

+106
-17
lines changed

3 files changed

+106
-17
lines changed

go/extractor/project/BUILD.bazel

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/extractor/project/project.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ type GoModule struct {
3636
Module *modfile.File // The parsed contents of the `go.mod` file
3737
}
3838

39+
// Tries to find the Go toolchain version required for this module.
40+
func (module *GoModule) RequiredGoVersion() util.SemVer {
41+
if module.Module != nil && module.Module.Toolchain != nil {
42+
return util.NewSemVer(module.Module.Toolchain.Name)
43+
} else if module.Module != nil && module.Module.Go != nil {
44+
return util.NewSemVer(module.Module.Go.Version)
45+
} else {
46+
return tryReadGoDirective(module.Path)
47+
}
48+
}
49+
3950
// Represents information about a Go project workspace: this may either be a folder containing
4051
// a `go.work` file or a collection of `go.mod` files.
4152
type GoWorkspace struct {
@@ -54,24 +65,23 @@ type GoVersionInfo = util.SemVer
5465
// 1. The Go version specified in the `go.work` file, if any.
5566
// 2. The greatest Go version specified in any `go.mod` file, if any.
5667
func (workspace *GoWorkspace) RequiredGoVersion() util.SemVer {
57-
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
58-
// If we have parsed a `go.work` file, return the version number from it.
68+
// If we have parsed a `go.work` file, we prioritise versions from it over those in individual `go.mod`
69+
// files. We are interested in toolchain versions, so if there is an explicit toolchain declaration in
70+
// a `go.work` file, we use that. Otherwise, we fall back to the language version in the `go.work` file
71+
// and use that as toolchain version. If we didn't parse a `go.work` file, then we try to find the
72+
// greatest version contained in `go.mod` files.
73+
if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Toolchain != nil {
74+
return util.NewSemVer(workspace.WorkspaceFile.Toolchain.Name)
75+
} else if workspace.WorkspaceFile != nil && workspace.WorkspaceFile.Go != nil {
5976
return util.NewSemVer(workspace.WorkspaceFile.Go.Version)
6077
} else if workspace.Modules != nil && len(workspace.Modules) > 0 {
6178
// Otherwise, if we have `go.work` files, find the greatest Go version in those.
6279
var greatestVersion util.SemVer = nil
6380
for _, module := range workspace.Modules {
64-
if module.Module != nil && module.Module.Go != nil {
65-
// If we have parsed the file, retrieve the version number we have already obtained.
66-
modVersion := util.NewSemVer(module.Module.Go.Version)
67-
if greatestVersion == nil || modVersion.IsNewerThan(greatestVersion) {
68-
greatestVersion = modVersion
69-
}
70-
} else {
71-
modVersion := tryReadGoDirective(module.Path)
72-
if modVersion != nil && (greatestVersion == nil || modVersion.IsNewerThan(greatestVersion)) {
73-
greatestVersion = modVersion
74-
}
81+
modVersion := module.RequiredGoVersion()
82+
83+
if modVersion != nil && (greatestVersion == nil || modVersion.IsNewerThan(greatestVersion)) {
84+
greatestVersion = modVersion
7585
}
7686
}
7787

go/extractor/project/project_test.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"path/filepath"
55
"testing"
66

7+
"github.com/github/codeql-go/extractor/util"
78
"golang.org/x/mod/modfile"
89
)
910

@@ -28,14 +29,18 @@ func TestStartsWithAnyOf(t *testing.T) {
2829
testStartsWithAnyOf(t, filepath.Join("foo", "bar"), filepath.Join("foo", "baz"), false)
2930
}
3031

31-
func testHasInvalidToolchainVersion(t *testing.T, contents string) bool {
32-
modFile, err := modfile.Parse("test.go", []byte(contents), nil)
32+
func parseModFile(t *testing.T, contents string) *modfile.File {
33+
modFile, err := modfile.Parse("go.mod", []byte(contents), nil)
3334

3435
if err != nil {
3536
t.Errorf("Unable to parse %s: %s.\n", contents, err.Error())
3637
}
3738

38-
return hasInvalidToolchainVersion(modFile)
39+
return modFile
40+
}
41+
42+
func testHasInvalidToolchainVersion(t *testing.T, contents string) bool {
43+
return hasInvalidToolchainVersion(parseModFile(t, contents))
3944
}
4045

4146
func TestHasInvalidToolchainVersion(t *testing.T) {
@@ -62,3 +67,74 @@ func TestHasInvalidToolchainVersion(t *testing.T) {
6267
}
6368
}
6469
}
70+
71+
func parseWorkFile(t *testing.T, contents string) *modfile.WorkFile {
72+
workFile, err := modfile.ParseWork("go.work", []byte(contents), nil)
73+
74+
if err != nil {
75+
t.Errorf("Unable to parse %s: %s.\n", contents, err.Error())
76+
}
77+
78+
return workFile
79+
}
80+
81+
func TestRequiredGoVersion(t *testing.T) {
82+
type ModVersionPair struct {
83+
FileContents string
84+
ExpectedVersion string
85+
}
86+
87+
modules := []ModVersionPair{
88+
{"go 1.20", "v1.20"},
89+
{"go 1.21.2", "v1.21.2"},
90+
{"go 1.21rc1", "v1.21.0-rc1"},
91+
{"go 1.21rc1\ntoolchain go1.22.0", "v1.22.0"},
92+
{"go 1.21rc1\ntoolchain go1.22rc1", "v1.22.0-rc1"},
93+
}
94+
95+
for _, testData := range modules {
96+
// `go.mod` and `go.work` files have mostly the same format
97+
modFile := parseModFile(t, testData.FileContents)
98+
workFile := parseWorkFile(t, testData.FileContents)
99+
mod := GoModule{
100+
Path: "test", // irrelevant
101+
Module: modFile,
102+
}
103+
work := GoWorkspace{
104+
WorkspaceFile: workFile,
105+
}
106+
107+
result := mod.RequiredGoVersion()
108+
if result == nil {
109+
t.Errorf(
110+
"Expected mod.RequiredGoVersion() to return %s for the below `go.mod` file, but got nothing:\n%s",
111+
testData.ExpectedVersion,
112+
testData.FileContents,
113+
)
114+
} else if result != util.NewSemVer(testData.ExpectedVersion) {
115+
t.Errorf(
116+
"Expected mod.RequiredGoVersion() to return %s for the below `go.mod` file, but got %s:\n%s",
117+
testData.ExpectedVersion,
118+
result,
119+
testData.FileContents,
120+
)
121+
}
122+
123+
result = work.RequiredGoVersion()
124+
if result == nil {
125+
t.Errorf(
126+
"Expected mod.RequiredGoVersion() to return %s for the below `go.work` file, but got nothing:\n%s",
127+
testData.ExpectedVersion,
128+
testData.FileContents,
129+
)
130+
} else if result != util.NewSemVer(testData.ExpectedVersion) {
131+
t.Errorf(
132+
"Expected mod.RequiredGoVersion() to return %s for the below `go.work` file, but got %s:\n%s",
133+
testData.ExpectedVersion,
134+
result,
135+
testData.FileContents,
136+
)
137+
}
138+
}
139+
140+
}

0 commit comments

Comments
 (0)