176 lines
5.8 KiB
Go
176 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"go/format"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// **gofmt and go vet are the unit level's** (novox/hq ADR 0529 §1; issue 1887 D7-go): what every repository's script ran
|
|
// beside its tests, run for every repository by the build module that runs its unit level. In each go.mod directory the
|
|
// level runs, every Go file outside testdata and vendor must be as gofmt writes it, and `go vet ./...` must pass with no
|
|
// tag and with every build tag the directory's files name together (integration, isolation, ...), so code behind a tag
|
|
// nothing else compiles is vetted too. A failure is the level's, naming the files or vet's lines.
|
|
|
|
// buildTag is one name in a //go:build line.
|
|
var buildTag = regexp.MustCompile(`[A-Za-z_][A-Za-z0-9_.]*`)
|
|
|
|
// notATag are the names of a //go:build line that are no tag a run sets: the platforms, cgo, ignore, go versions.
|
|
var notATag = map[string]bool{"ignore": true, "cgo": true, "linux": true, "darwin": true, "windows": true, "freebsd": true,
|
|
"openbsd": true, "netbsd": true, "unix": true, "plan9": true, "js": true, "wasip1": true, "android": true, "ios": true,
|
|
"amd64": true, "arm64": true, "386": true, "arm": true, "riscv64": true, "wasm": true, "ppc64le": true, "s390x": true}
|
|
|
|
// lintable walks a go.mod directory's own Go files: not those of testdata, vendor, or a directory holding a go.mod of its
|
|
// own (another directory of the level, linted there).
|
|
func lintable(dir string, each func(path string) error) error {
|
|
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
if path != dir {
|
|
switch d.Name() {
|
|
case "testdata", "vendor":
|
|
return filepath.SkipDir
|
|
}
|
|
if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
|
|
return filepath.SkipDir
|
|
}
|
|
if strings.HasPrefix(d.Name(), ".") {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(path, ".go") {
|
|
return each(path)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// unformatted are the Go files of a directory gofmt would change, relative to the tree, and the build tags they name.
|
|
func unformatted(tree, d string) (files, tags []string, err error) {
|
|
seen := map[string]bool{}
|
|
err = lintable(filepath.Join(tree, d), func(path string) error {
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out, ferr := format.Source(src); ferr != nil || !bytes.Equal(out, src) {
|
|
rel, _ := filepath.Rel(tree, path)
|
|
files = append(files, filepath.ToSlash(rel))
|
|
}
|
|
scan := bufio.NewScanner(bytes.NewReader(src))
|
|
for scan.Scan() {
|
|
line := strings.TrimSpace(scan.Text())
|
|
if strings.HasPrefix(line, "package ") {
|
|
break
|
|
}
|
|
if expr, ok := strings.CutPrefix(line, "//go:build "); ok {
|
|
for _, tag := range buildTag.FindAllString(expr, -1) {
|
|
if !notATag[tag] && !strings.HasPrefix(tag, "go1") && !seen[tag] {
|
|
seen[tag] = true
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
sort.Strings(files)
|
|
sort.Strings(tags)
|
|
return files, tags, err
|
|
}
|
|
|
|
// lint is gofmt and go vet of one go.mod directory d of the tree, run with run in the directory's go environment; a
|
|
// failure of either is a Failure of the level naming what failed.
|
|
func (m *Module) lint(ctx context.Context, run forgekey.Runner, tree, d string, env []string) ([]Failure, error) {
|
|
files, tags, err := unformatted(tree, d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading the Go files of %s: %w", d, err)
|
|
}
|
|
var failed []Failure
|
|
if len(files) > 0 {
|
|
failed = append(failed, buildstep.ConsoleFailure(d, "gofmt", "not as gofmt writes them (run gofmt -w): "+strings.Join(files, ", ")))
|
|
}
|
|
sets := [][]string{nil}
|
|
if len(tags) > 0 {
|
|
sets = append(sets, tags)
|
|
}
|
|
for _, set := range sets {
|
|
args := []string{"-C", filepath.Join(tree, d), "vet"}
|
|
name := "go vet"
|
|
if len(set) > 0 {
|
|
args = append(args, "-tags", strings.Join(set, ","))
|
|
name += " -tags " + strings.Join(set, ",")
|
|
}
|
|
args = append(args, "./...")
|
|
r := run(ctx, env, "go", args...)
|
|
if r.Err != "" {
|
|
return nil, fmt.Errorf("%s in %s could not be run: %s", name, d, firstLine(r.Err+"\n"+r.Stderr))
|
|
}
|
|
if r.Status != 0 {
|
|
failed = append(failed, buildstep.ConsoleFailure(d, name, r.Stderr+"\n"+r.Stdout))
|
|
}
|
|
}
|
|
return failed, nil
|
|
}
|
|
|
|
// lintAll lints each directory, up to m.parallel() at once like the level's tests; the failures join in directory order
|
|
// and the error is the first directory's, so the answer does not depend on which ended first (issue 1930).
|
|
func (m *Module) lintAll(ctx context.Context, run forgekey.Runner, tree string, dirs []string, env func(d string) []string) ([]Failure, error) {
|
|
// The ask's phases say it (issue 1890): lint of the directories, then their tests.
|
|
buildstep.Phase(ctx, fmt.Sprintf("unit: lint of %d directories", len(dirs)))
|
|
defer buildstep.Phase(ctx, fmt.Sprintf("unit: tests of %d directories", len(dirs)))
|
|
type answer struct {
|
|
failed []Failure
|
|
err error
|
|
}
|
|
answers := make([]answer, len(dirs))
|
|
slots := make(chan struct{}, m.parallel())
|
|
var wg sync.WaitGroup
|
|
var crash any // a panic of a lint goes on in the caller, as a panic of a test run does
|
|
var crashMu sync.Mutex
|
|
for i, d := range dirs {
|
|
wg.Add(1)
|
|
slots <- struct{}{}
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() { <-slots }()
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
crashMu.Lock()
|
|
crash = p
|
|
crashMu.Unlock()
|
|
}
|
|
}()
|
|
answers[i].failed, answers[i].err = m.lint(ctx, run, tree, d, env(d))
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if crash != nil {
|
|
panic(crash)
|
|
}
|
|
var failed []Failure
|
|
for _, a := range answers {
|
|
if a.err != nil {
|
|
return nil, a.err
|
|
}
|
|
failed = append(failed, a.failed...)
|
|
}
|
|
return failed, nil
|
|
}
|