181 lines
6.5 KiB
Go
181 lines
6.5 KiB
Go
// Package sdkjudge refuses mesh-sdk a manifest judge (novox/hq ADR 0578 §4): a package every module compiles in carries
|
|
// the contract only, so it holds no function that gathers a manifest's problems and no table of what a manifest may
|
|
// declare. The judge is a package of mesh-delivery that only the indexer and the checker use, and a reader decodes a
|
|
// manifest and takes the indexer's verdict (ADR 0578 §3).
|
|
//
|
|
// It reads mesh-sdk's source as the checker places it beside this check (MESH_CHECK_BESIDE, the repository mesh-sdk: at its
|
|
// trunk, or at the head of the group's pull request when the group holds one), so a change adding either to mesh-sdk in a
|
|
// delivery group with a catalogue change is refused here. A change to mesh-sdk alone is checked by mesh-sdk's own check,
|
|
// which does not run this package: the catalogue's check sees the SDK only beside a catalogue change.
|
|
//
|
|
// What it refuses, in the non-test Go files of mesh-sdk's go/ tree:
|
|
// - a function or method named for problems (a name ending in Problems or Problem) that takes a Manifest or a Shelf, and
|
|
// a function named ParseManifest, ParseManifestWith, ParseManifestForInputs, ValidateManifest... or ValidateShelf...: the
|
|
// judge's entry points;
|
|
// - in the package catalogue, a package-level variable named for what is allowed, permitted, removable, reserved or
|
|
// forbidden whose value is a map or a list: an allow-list table.
|
|
//
|
|
// The composer's refusals of composition itself (what a machine can be composed with) are not a judgement of a manifest
|
|
// (ADR 0578 §4, clause 5; issue 1585 tracks moving them): they are named in composition, each with why, and the list only
|
|
// shrinks, since a name that no longer exists is refused too.
|
|
package sdkjudge
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// composition is every function or table the check lets stay in mesh-sdk, as "path-from-go/:name", and why. It names no
|
|
// manifest judgement: each is the composer's refusal of composition itself (ADR 0578 clause 5).
|
|
var composition = map[string]string{}
|
|
|
|
// Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, and the
|
|
// entries of composition it no longer holds.
|
|
func Problems(root string) ([]string, error) {
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
if n := d.Name(); n == "testdata" || n == "vendor" || strings.HasPrefix(n, ".") {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
rel, _ := filepath.Rel(root, path)
|
|
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", rel, err)
|
|
}
|
|
inCatalogue := filepath.Base(filepath.Dir(path)) == "catalogue"
|
|
for _, d := range file.Decls {
|
|
switch d := d.(type) {
|
|
case *ast.FuncDecl:
|
|
if why := judgeFunc(d); why != "" {
|
|
out = append(out, note(composition, seen, rel, d.Name.Name, why))
|
|
}
|
|
case *ast.GenDecl:
|
|
if !inCatalogue || d.Tok != token.VAR {
|
|
continue
|
|
}
|
|
for _, spec := range d.Specs {
|
|
v, _ := spec.(*ast.ValueSpec)
|
|
if v == nil {
|
|
continue
|
|
}
|
|
for i, name := range v.Names {
|
|
if i < len(v.Values) && allowName.MatchString(name.Name) && isTable(v.Values[i]) {
|
|
out = append(out, note(composition, seen, rel, name.Name,
|
|
"a table of what a manifest may declare (an allow-list)"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var kept []string
|
|
for _, line := range out {
|
|
if line != "" {
|
|
kept = append(kept, line)
|
|
}
|
|
}
|
|
for key, why := range composition {
|
|
if !seen[key] {
|
|
kept = append(kept, fmt.Sprintf("composition lists %s (%s), which mesh-sdk no longer holds: take it off the list", key, why))
|
|
}
|
|
}
|
|
sort.Strings(kept)
|
|
return kept, nil
|
|
}
|
|
|
|
// note is the line refusing name in rel for why, or "" when composition lets it stay.
|
|
func note(composition map[string]string, seen map[string]bool, rel, name, why string) string {
|
|
key := rel + ":" + name
|
|
if _, ok := composition[key]; ok {
|
|
seen[key] = true
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%s: %s is %s; mesh-sdk carries the contract only, the judge is the indexer's and the checker's (ADR 0578 §4)", rel, name, why)
|
|
}
|
|
|
|
// allowName is what names a table of what is allowed.
|
|
var allowName = regexp.MustCompile(`(?i)allow|permit|removable|reserved|forbid|denied`)
|
|
|
|
// isTable says whether an expression is a map or a list literal of more than one entry.
|
|
func isTable(e ast.Expr) bool {
|
|
c, ok := e.(*ast.CompositeLit)
|
|
if !ok || len(c.Elts) < 2 {
|
|
return false
|
|
}
|
|
switch c.Type.(type) {
|
|
case *ast.MapType, *ast.ArrayType:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// judgeFunc says why a function is a manifest judge, or "".
|
|
func judgeFunc(f *ast.FuncDecl) string {
|
|
name := f.Name.Name
|
|
if judgeEntry.MatchString(name) {
|
|
return "a function that judges a manifest"
|
|
}
|
|
if !strings.HasSuffix(name, "Problems") && !strings.HasSuffix(name, "Problem") {
|
|
return ""
|
|
}
|
|
if takes(f.Recv) || takes(f.Type.Params) {
|
|
return "a function that gathers a manifest's problems"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// judgeEntry is the names of the judge's entry points: the strict parsers and the validators of a manifest or a shelf.
|
|
var judgeEntry = regexp.MustCompile(`^(ParseManifest(With|ForInputs)?|Validate(Manifest|Shelf)\w*)$`)
|
|
|
|
// takes says whether a list of parameters names a Manifest or a Shelf anywhere in a type.
|
|
func takes(fields *ast.FieldList) bool {
|
|
if fields == nil {
|
|
return false
|
|
}
|
|
found := false
|
|
for _, f := range fields.List {
|
|
ast.Inspect(f.Type, func(n ast.Node) bool {
|
|
if id, ok := n.(*ast.Ident); ok && (id.Name == "Manifest" || id.Name == "Shelf") {
|
|
found = true
|
|
}
|
|
return !found
|
|
})
|
|
}
|
|
return found
|
|
}
|
|
|
|
// BesideDir is where the checker places mesh-sdk's clone, as a directory under MESH_CHECK_BESIDE; its go/ tree is judged.
|
|
func BesideDir() (string, error) {
|
|
beside := os.Getenv("MESH_CHECK_BESIDE")
|
|
if beside == "" {
|
|
return "", fmt.Errorf("NOT CHECKED: MESH_CHECK_BESIDE is not set, so mesh-sdk's source is not beside this check; the checker places the SDK there (besideRules)")
|
|
}
|
|
dir := filepath.Join(beside, "mesh-sdk", "go")
|
|
if _, err := os.Stat(dir); err != nil {
|
|
return "", fmt.Errorf("NOT CHECKED: mesh-sdk is not beside this check in MESH_CHECK_BESIDE=%s: %w", beside, err)
|
|
}
|
|
return dir, nil
|
|
}
|