Skip to content

Commit 5500330

Browse files
authored
Merge pull request github#12341 from owen-mc/go-tools-status
Go: tools status page support
2 parents cacae95 + db5bd98 commit 5500330

File tree

32 files changed

+751
-7
lines changed

32 files changed

+751
-7
lines changed

go/extractor/cli/go-autobuilder/go-autobuilder.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/mod/semver"
1717

1818
"github.com/github/codeql-go/extractor/autobuilder"
19+
"github.com/github/codeql-go/extractor/diagnostics"
1920
"github.com/github/codeql-go/extractor/util"
2021
)
2122

@@ -203,7 +204,7 @@ func (m ModMode) argsForGoVersion(version string) []string {
203204
}
204205

205206
// addVersionToMod add a go version directive, e.g. `go 1.14` to a `go.mod` file.
206-
func addVersionToMod(goMod []byte, version string) bool {
207+
func addVersionToMod(version string) bool {
207208
cmd := exec.Command("go", "mod", "edit", "-go="+version)
208209
return util.RunCmd(cmd)
209210
}
@@ -249,12 +250,29 @@ func main() {
249250
depMode := GoGetNoModules
250251
modMode := ModUnset
251252
needGopath := true
253+
goDirectiveFound := false
252254
if _, present := os.LookupEnv("GO111MODULE"); !present {
253255
os.Setenv("GO111MODULE", "auto")
254256
}
255257
if util.FileExists("go.mod") {
256258
depMode = GoGetWithModules
257259
needGopath = false
260+
versionRe := regexp.MustCompile(`(?m)^go[ \t\r]+([0-9]+\.[0-9]+)$`)
261+
goMod, err := ioutil.ReadFile("go.mod")
262+
if err != nil {
263+
log.Println("Failed to read go.mod to check for missing Go version")
264+
} else {
265+
matches := versionRe.FindSubmatch(goMod)
266+
if matches != nil {
267+
goDirectiveFound = true
268+
if len(matches) > 1 {
269+
goDirectiveVersion := "v" + string(matches[1])
270+
if semver.Compare(goDirectiveVersion, getEnvGoSemVer()) >= 0 {
271+
diagnostics.EmitNewerGoVersionNeeded()
272+
}
273+
}
274+
}
275+
}
258276
log.Println("Found go.mod, enabling go modules")
259277
} else if util.FileExists("Gopkg.toml") {
260278
depMode = Dep
@@ -283,18 +301,15 @@ func main() {
283301
// we work around this by adding an explicit go version of 1.13, which is the last version
284302
// where this is not an issue
285303
if depMode == GoGetWithModules {
286-
goMod, err := ioutil.ReadFile("go.mod")
287-
if err != nil {
288-
log.Println("Failed to read go.mod to check for missing Go version")
289-
} else if versionRe := regexp.MustCompile(`(?m)^go[ \t\r]+[0-9]+\.[0-9]+$`); !versionRe.Match(goMod) {
304+
if !goDirectiveFound {
290305
// if the go.mod does not contain a version line
291306
modulesTxt, err := ioutil.ReadFile("vendor/modules.txt")
292307
if err != nil {
293308
log.Println("Failed to read vendor/modules.txt to check for mismatched Go version")
294309
} else if explicitRe := regexp.MustCompile("(?m)^## explicit$"); !explicitRe.Match(modulesTxt) {
295310
// and the modules.txt does not contain an explicit annotation
296311
log.Println("Adding a version directive to the go.mod file as the modules.txt does not have explicit annotations")
297-
if !addVersionToMod(goMod, "1.13") {
312+
if !addVersionToMod("1.13") {
298313
log.Println("Failed to add a version to the go.mod file to fix explicitly required package bug; not using vendored dependencies")
299314
modMode = ModMod
300315
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package diagnostics
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
"time"
10+
)
11+
12+
type sourceStruct struct {
13+
Id string `json:"id"`
14+
Name string `json:"name"`
15+
ExtractorName string `json:"extractorName"`
16+
}
17+
18+
type diagnosticSeverity string
19+
20+
const (
21+
severityError diagnosticSeverity = "error"
22+
severityWarning diagnosticSeverity = "warning"
23+
severityNote diagnosticSeverity = "note"
24+
)
25+
26+
type visibilityStruct struct {
27+
StatusPage bool `json:"statusPage"` // True if the message should be displayed on the status page (defaults to false)
28+
CliSummaryTable bool `json:"cliSummaryTable"` // True if the message should be counted in the diagnostics summary table printed by `codeql database analyze` (defaults to false)
29+
Telemetry bool `json:"telemetry"` // True if the message should be sent to telemetry (defaults to false)
30+
}
31+
32+
var fullVisibility *visibilityStruct = &visibilityStruct{true, true, true}
33+
34+
type locationStruct struct {
35+
File string `json:"file,omitempty"`
36+
StartLine int `json:"startLine,omitempty"`
37+
StartColumn int `json:"startColumn,omitempty"`
38+
EndLine int `json:"endLine,omitempty"`
39+
EndColumn int `json:"endColumn,omitempty"`
40+
}
41+
42+
var noLocation *locationStruct = nil
43+
44+
type diagnostic struct {
45+
Timestamp string `json:"timestamp"`
46+
Source sourceStruct `json:"source"`
47+
MarkdownMessage string `json:"markdownMessage"`
48+
Severity string `json:"severity"`
49+
Visibility *visibilityStruct `json:"visibility,omitempty"` // Use a pointer so that it is omitted if nil
50+
Location *locationStruct `json:"location,omitempty"` // Use a pointer so that it is omitted if nil
51+
}
52+
53+
var diagnosticsEmitted, diagnosticsLimit uint = 0, 100
54+
var noDiagnosticDirPrinted bool = false
55+
56+
func emitDiagnostic(sourceid, sourcename, markdownMessage string, severity diagnosticSeverity, visibility *visibilityStruct, location *locationStruct) {
57+
if diagnosticsEmitted < diagnosticsLimit {
58+
diagnosticsEmitted += 1
59+
60+
diagnosticDir := os.Getenv("CODEQL_EXTRACTOR_GO_DIAGNOSTIC_DIR")
61+
if diagnosticDir == "" {
62+
if !noDiagnosticDirPrinted {
63+
log.Println("No diagnostic directory set, so not emitting diagnostic")
64+
noDiagnosticDirPrinted = true
65+
}
66+
return
67+
}
68+
69+
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000") + "Z"
70+
71+
var d diagnostic
72+
73+
if diagnosticsEmitted < diagnosticsLimit {
74+
d = diagnostic{
75+
timestamp,
76+
sourceStruct{sourceid, sourcename, "go"},
77+
markdownMessage,
78+
string(severity),
79+
visibility,
80+
location,
81+
}
82+
} else {
83+
d = diagnostic{
84+
timestamp,
85+
sourceStruct{"go/autobuilder/diagnostic-limit-reached", "Diagnostics limit exceeded", "go"},
86+
fmt.Sprintf("CodeQL has produced more than the maximum number of diagnostics. Only the first %d have been reported.", diagnosticsLimit),
87+
string(severityWarning),
88+
fullVisibility,
89+
noLocation,
90+
}
91+
}
92+
93+
content, err := json.Marshal(d)
94+
if err != nil {
95+
log.Println(err)
96+
return
97+
}
98+
99+
targetFile, err := os.CreateTemp(diagnosticDir, "go-extractor.*.json")
100+
if err != nil {
101+
log.Println("Failed to create diagnostic file: ")
102+
log.Println(err)
103+
return
104+
}
105+
defer func() {
106+
if err := targetFile.Close(); err != nil {
107+
log.Println("Failed to close diagnostic file:")
108+
log.Println(err)
109+
}
110+
}()
111+
112+
_, err = targetFile.Write(content)
113+
if err != nil {
114+
log.Println("Failed to write to diagnostic file: ")
115+
log.Println(err)
116+
}
117+
}
118+
}
119+
120+
func EmitPackageDifferentOSArchitecture(pkgPath string) {
121+
emitDiagnostic(
122+
"go/autobuilder/package-different-os-architecture",
123+
"Package "+pkgPath+" is intended for a different OS or architecture",
124+
"Make sure the `GOOS` and `GOARCH` [environment variables are correctly set](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow). Alternatively, [change your OS and architecture](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#using-a-github-hosted-runner).",
125+
severityWarning,
126+
fullVisibility,
127+
noLocation,
128+
)
129+
}
130+
131+
const maxNumPkgPaths = 5
132+
133+
func EmitCannotFindPackages(pkgPaths []string) {
134+
numPkgPaths := len(pkgPaths)
135+
136+
ending := "s"
137+
if numPkgPaths == 1 {
138+
ending = ""
139+
}
140+
141+
numPrinted := numPkgPaths
142+
truncated := false
143+
if numPrinted > maxNumPkgPaths {
144+
numPrinted = maxNumPkgPaths
145+
truncated = true
146+
}
147+
148+
secondLine := "`" + strings.Join(pkgPaths[0:numPrinted], "`, `") + "`"
149+
if truncated {
150+
secondLine += fmt.Sprintf(" and %d more", numPkgPaths-maxNumPkgPaths)
151+
}
152+
153+
emitDiagnostic(
154+
"go/autobuilder/package-not-found",
155+
fmt.Sprintf("%d package%s could not be found", numPkgPaths, ending),
156+
"The following packages could not be found.\n\n"+secondLine+"\n\nCheck that the paths are correct and make sure any private packages can be accessed. If any of the packages are present in the repository then you may need a [custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages).",
157+
severityError,
158+
fullVisibility,
159+
noLocation,
160+
)
161+
}
162+
163+
func EmitNewerGoVersionNeeded() {
164+
emitDiagnostic(
165+
"go/autobuilder/newer-go-version-needed",
166+
"Newer Go version needed",
167+
"The detected version of Go is lower than the version specified in `go.mod`. [Install a newer version](https://github.com/actions/setup-go#basic).",
168+
severityError,
169+
fullVisibility,
170+
noLocation,
171+
)
172+
}
173+
174+
func EmitGoFilesFoundButNotProcessed() {
175+
emitDiagnostic(
176+
"go/autobuilder/go-files-found-but-not-processed",
177+
"Go files were found but not processed",
178+
"[Specify a custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages) that includes one or more `go build` commands to build the `.go` files to be analyzed.",
179+
severityError,
180+
fullVisibility,
181+
noLocation,
182+
)
183+
}

go/extractor/extractor.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"time"
2323

2424
"github.com/github/codeql-go/extractor/dbscheme"
25+
"github.com/github/codeql-go/extractor/diagnostics"
2526
"github.com/github/codeql-go/extractor/srcarchive"
2627
"github.com/github/codeql-go/extractor/trap"
2728
"github.com/github/codeql-go/extractor/util"
@@ -97,6 +98,13 @@ func ExtractWithFlags(buildFlags []string, patterns []string) error {
9798

9899
if len(pkgs) == 0 {
99100
log.Println("No packages found.")
101+
102+
wd, err := os.Getwd()
103+
if err != nil {
104+
log.Printf("Warning: failed to get working directory: %s\n", err.Error())
105+
} else if util.FindGoFiles(wd) {
106+
diagnostics.EmitGoFilesFoundButNotProcessed()
107+
}
100108
}
101109

102110
log.Println("Extracting universe scope.")
@@ -118,6 +126,8 @@ func ExtractWithFlags(buildFlags []string, patterns []string) error {
118126
log.Printf("Done running go list deps: resolved %d packages.", len(pkgInfos))
119127
}
120128

129+
pkgsNotFound := make([]string, 0, len(pkgs))
130+
121131
// Do a post-order traversal and extract the package scope of each package
122132
packages.Visit(pkgs, func(pkg *packages.Package) bool {
123133
return true
@@ -144,13 +154,27 @@ func ExtractWithFlags(buildFlags []string, patterns []string) error {
144154
if len(pkg.Errors) != 0 {
145155
log.Printf("Warning: encountered errors extracting package `%s`:", pkg.PkgPath)
146156
for i, err := range pkg.Errors {
147-
log.Printf(" %s", err.Error())
157+
errString := err.Error()
158+
log.Printf(" %s", errString)
159+
160+
if strings.Contains(errString, "build constraints exclude all Go files in ") {
161+
// `err` is a NoGoError from the package cmd/go/internal/load, which we cannot access as it is internal
162+
diagnostics.EmitPackageDifferentOSArchitecture(pkg.PkgPath)
163+
} else if strings.Contains(errString, "cannot find package") ||
164+
strings.Contains(errString, "no required module provides package") {
165+
pkgsNotFound = append(pkgsNotFound, pkg.PkgPath)
166+
}
148167
extraction.extractError(tw, err, lbl, i)
149168
}
150169
}
170+
151171
log.Printf("Done extracting types for package %s.", pkg.PkgPath)
152172
})
153173

174+
if len(pkgsNotFound) > 0 {
175+
diagnostics.EmitCannotFindPackages(pkgsNotFound)
176+
}
177+
154178
for _, pkg := range pkgs {
155179
pkgInfo, ok := pkgInfos[pkg.PkgPath]
156180
if !ok || pkgInfo.PkgDir == "" {

go/extractor/util/util.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"io"
7+
"io/fs"
78
"log"
89
"os"
910
"os/exec"
@@ -281,3 +282,18 @@ func EscapeTrapSpecialChars(s string) string {
281282
s = strings.ReplaceAll(s, "#", "&num;")
282283
return s
283284
}
285+
286+
func FindGoFiles(root string) bool {
287+
found := false
288+
filepath.WalkDir(root, func(s string, d fs.DirEntry, e error) error {
289+
if e != nil {
290+
return e
291+
}
292+
if filepath.Ext(d.Name()) == ".go" {
293+
found = true
294+
return filepath.SkipAll
295+
}
296+
return nil
297+
})
298+
return found
299+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"markdownMessage": "Make sure the `GOOS` and `GOARCH` [environment variables are correctly set](https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow). Alternatively, [change your OS and architecture](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#using-a-github-hosted-runner).",
3+
"severity": "warning",
4+
"source": {
5+
"extractorName": "go",
6+
"id": "go/autobuilder/package-different-os-architecture",
7+
"name": "Package syscall/js is intended for a different OS or architecture"
8+
},
9+
"visibility": {
10+
"cliSummaryTable": true,
11+
"statusPage": true,
12+
"telemetry": true
13+
}
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import sys
2+
3+
from create_database_utils import *
4+
from diagnostics_test_utils import *
5+
6+
os.environ['LGTM_INDEX_IMPORT_PATH'] = "test"
7+
run_codeql_database_create([], lang="go", source="work", db=None)
8+
9+
check_diagnostics()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
go 1.18
2+
3+
module test

go/ql/integration-tests/all-platforms/go/diagnostics/build-constraints-exclude-all-go-files/work/go.sum

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package test
2+
3+
import "syscall/js"
4+
5+
func Test() {
6+
var x js.Error
7+
_ = x
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"markdownMessage": "[Specify a custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages) that includes one or more `go build` commands to build the `.go` files to be analyzed.",
3+
"severity": "error",
4+
"source": {
5+
"extractorName": "go",
6+
"id": "go/autobuilder/go-files-found-but-not-processed",
7+
"name": "Go files were found but not processed"
8+
},
9+
"visibility": {
10+
"cliSummaryTable": true,
11+
"statusPage": true,
12+
"telemetry": true
13+
}
14+
}

0 commit comments

Comments
 (0)