Files
mesh-catalog/checks/particulars/main.go
T
jschoubben a3b51a9d9a
mesh/merge-gate head a3b51a9d9a02 merged into trunk db67a0e55ec3 — pass: the change touches no module of the mesh's graph
mesh/repo-check head a3b51a9d9a02 merged into trunk db67a0e55ec3 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
mesh/check failed: unit: nfsserver/cmd/nfs-server.TestARealRootOwnedFolderIsRefused: --- FAIL: TestARealRootOwnedFolderIsRefused (0.00s) | a3b51a9d9a0…
issue 1838: catch node names in paths, hyphenated and lowerCamel forms; test the exit codes
The forge blank hid any novox/ path segment, and joined names passed; read covered from the root.
2026-10-22 14:18:26 +02:00

160 lines
5.1 KiB
Go

// Command particulars keeps this mesh's node names out of the modules it covers (novox/hq ADR 0507 §9,
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
// fixture of a covered module names a node of this one.
//
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
// they may be written. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
// before a line is read.
//
// go run . <catalogue root>
//
// Exits 1 naming file:line and the word, 2 when a list cannot be read.
package main
import (
"bufio"
"bytes"
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// forge is the forge's host and organisation path, blanked before a line is read; a path segment novox/ elsewhere
// (testdata/novox/x, /srv/novox/data) is not blanked.
var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w.-])novox/[\w.-]+`)
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
os.Exit(check(root))
}
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
func check(root string) int {
names, err := readList(filepath.Join(root, "particulars"))
if err == nil && len(names) == 0 {
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
}
var covered []string
if err == nil {
covered, err = readList(filepath.Join(root, "checks/particulars/covered"))
if err == nil && len(covered) == 0 {
err = fmt.Errorf("covered lists no module — the check would pass on anything")
}
}
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
}
findings, err := run(root, names, covered)
if err != nil {
fmt.Fprintln(os.Stderr, "particulars:", err)
return 2
}
for _, f := range findings {
fmt.Println(f)
}
if len(findings) > 0 {
fmt.Printf("particulars: %d node name(s) in a module for any mesh (novox/hq ADR 0507 §9) — say what it shows, not where it was seen\n", len(findings))
return 1
}
fmt.Printf("particulars: none of %d node names in the %d covered modules\n", len(names), len(covered))
return 0
}
// readList reads a list: one entry a line; blank lines and lines starting with # are not entries.
func readList(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var out []string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out = append(out, line)
}
return out, sc.Err()
}
// run reads every text file of each covered module and returns the findings, sorted.
func run(root string, names, covered []string) ([]string, error) {
var out []string
for _, m := range covered {
dir := filepath.Join(root, "modules", m)
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case "node_modules", "dist", "vendor", ".git":
return filepath.SkipDir
}
return nil
}
rel, _ := filepath.Rel(root, path)
// A path is read too: a testdata directory named after a node is a finding.
for _, n := range hits(filepath.ToSlash(rel), names) {
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
}
if d.Name() == "go.sum" {
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return err
}
if bytes.IndexByte(b, 0) >= 0 {
return nil
}
out = append(out, scan(filepath.ToSlash(rel), string(b), names)...)
return nil
})
if err != nil {
return nil, err
}
}
sort.Strings(out)
return out, nil
}
// scan returns a finding for each node name in src, whole-word and in any case.
func scan(file, src string, names []string) []string {
var out []string
for i, line := range strings.Split(src, "\n") {
for _, n := range hits(line, names) {
out = append(out, fmt.Sprintf("%s:%d: node name %q — %s", file, i+1, n, strings.Join(strings.Fields(line), " ")))
}
}
return out
}
// hits returns the node names one line holds: as a word (a hyphen or underscore parts it), as a part of a
// CamelCase identifier (TestAce, novoxTree), the forge's host and organisation path blanked first.
func hits(line string, names []string) []string {
line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) })
var out []string
for _, n := range names {
q := regexp.QuoteMeta(n)
camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:])
rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`)
if rx.MatchString(line) {
out = append(out, strings.ToLower(n))
}
}
return out
}