build/analysis/importsort: init

This adds an analyzer which enforces import sorting as per
CODING_STANDARDS.md. It is not yet enabled in nogo.

This effectively duplicates some logic that is present in goimports.
However, that logic is mostly within an 'internal' package in x/tools,
and would be somewhat tricky to make work within the framework of an
analysis.Analyser (as it expects to also mutate a file). Thus, we
end up writing the analysis logic ourselves. Tests are provided to make
sure this logic doesn't rot away.

We also move some common logic from 'noioutil' to a new 'lib' package,
and implement the last piece of the puzzle there: a code generator to
provide information about the toolchain's stdlib as a map/set.

Change-Id: Ia0f32d6f9122e13117d18ae781d8255c6e3a887d
Reviewed-on: https://review.monogon.dev/c/monogon/+/494
Reviewed-by: Leopold Schabel <leo@nexantic.com>
diff --git a/build/analysis/lib/BUILD.bazel b/build/analysis/lib/BUILD.bazel
new file mode 100644
index 0000000..42666bf
--- /dev/null
+++ b/build/analysis/lib/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "generated.go",
+        "stdlib_packages.go",
+    ],
+    importpath = "source.monogon.dev/build/analysis/lib",
+    visibility = ["//visibility:public"],
+)
+
+genrule(
+    name = "stdlib_packages",
+    outs = ["stdlib_packages.go"],
+    cmd = "$(location //build/analysis/lib/genstd) $@",
+    tools = ["//build/analysis/lib/genstd"],
+)
diff --git a/build/analysis/lib/generated.go b/build/analysis/lib/generated.go
new file mode 100644
index 0000000..209a73f
--- /dev/null
+++ b/build/analysis/lib/generated.go
@@ -0,0 +1,29 @@
+package lib
+
+import (
+	"go/ast"
+	"strings"
+)
+
+const (
+	genPrefix = "// Code generated"
+	genSuffix = "DO NOT EDIT."
+)
+
+// IsGeneratedFile returns true if the file is generated according to
+// https://golang.org/s/generatedcode and other heuristics.
+func IsGeneratedFile(file *ast.File) bool {
+	for _, c := range file.Comments {
+		for _, t := range c.List {
+			if strings.HasPrefix(t.Text, genPrefix) && strings.HasSuffix(t.Text, genSuffix) {
+				return true
+			}
+			// Generated testmain.go stubs from rules_go - for some reason, they don't
+			// contain the expected markers.
+			if strings.Contains(t.Text, "This package must be initialized before packages being tested.") {
+				return true
+			}
+		}
+	}
+	return false
+}
diff --git a/build/analysis/lib/genstd/BUILD.bazel b/build/analysis/lib/genstd/BUILD.bazel
new file mode 100644
index 0000000..044eb95
--- /dev/null
+++ b/build/analysis/lib/genstd/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/build/analysis/lib/genstd",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//build/toolbase/gotoolchain:go_default_library",
+        "@org_golang_x_tools//go/packages:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "genstd",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/build/analysis/lib/genstd/main.go b/build/analysis/lib/genstd/main.go
new file mode 100644
index 0000000..eae356b
--- /dev/null
+++ b/build/analysis/lib/genstd/main.go
@@ -0,0 +1,70 @@
+// This tool generates //build/analysis/lib:stdlib_packages.go, which contains a
+// set of all Go stdlib packges. This is generated ahead of time in the build
+// system as it can be an expensive operation that also depends on the presence
+// of a working `go` tool environment, so we want to do this as rarely as
+// possible.
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"golang.org/x/tools/go/packages"
+
+	"source.monogon.dev/build/toolbase/gotoolchain"
+)
+
+func main() {
+	os.Setenv("PATH", filepath.Dir(gotoolchain.Go))
+
+	gocache, err := os.MkdirTemp("/tmp", "gocache")
+	if err != nil {
+		panic(err)
+	}
+	defer os.RemoveAll(gocache)
+
+	gopath, err := os.MkdirTemp("/tmp", "gopath")
+	if err != nil {
+		panic(err)
+	}
+	defer os.RemoveAll(gopath)
+
+	os.Setenv("GOCACHE", gocache)
+	os.Setenv("GOPATH", gopath)
+
+	pkgs, err := packages.Load(nil, "std")
+	if err != nil {
+		panic(err)
+	}
+	sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].PkgPath < pkgs[j].PkgPath })
+
+	if len(os.Args) != 2 {
+		panic("must be called with output file name")
+	}
+
+	out, err := os.Create(os.Args[1])
+	if err != nil {
+		panic(err)
+	}
+	defer out.Close()
+
+	fmt.Fprintf(out, "// Code generated by //build/analysis/lib/genstd. DO NOT EDIT.\n")
+	fmt.Fprintf(out, "package lib\n\n")
+	fmt.Fprintf(out, "// StdlibPackages is a set of all package paths that are part of the Go standard\n")
+	fmt.Fprintf(out, "// library.\n")
+	fmt.Fprintf(out, "var StdlibPackages = map[string]bool{\n")
+	for _, pkg := range pkgs {
+		path := pkg.PkgPath
+		if strings.Contains(path, "/internal/") {
+			continue
+		}
+		if strings.HasPrefix(path, "vendor/") {
+			continue
+		}
+		fmt.Fprintf(out, "\t%q: true,\n", pkg.PkgPath)
+	}
+	fmt.Fprintf(out, "}\n")
+}