// 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{ "catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template", } // Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, but for // what composition lets stay (the map the caller gives; the test of the real tree gives the package's), and the // entries of composition it no longer holds. func Problems(root string, composition map[string]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 }