novox/hq is the name of the software's own record, whatever follows it; asus-g14 is the laptop's name in its vendor trigger file, declared in particulars as hardware asus-g14 (1838/AC2).
297 lines
10 KiB
Go
297 lines
10 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 any 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. Every directory under modules/ is read, with no list of covered modules, so a new module
|
|
// is covered from its first commit. 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.
|
|
// Some text holds a node name and is not one, so it is blanked before a line is read, each form exact: the
|
|
// software's module path (git.novox.be/novox/<repo>), the name of the software's record (novox/hq, followed by
|
|
// anything), the hardware model (asus-zephyrus-g14, ROG Zephyrus G14), and the software's own
|
|
// name and npm scope (Novox Mesh, novox-mesh, @novox/mesh-sdk with nothing before the @). Any other novox/<x>,
|
|
// the forge's host alone and a node after a word's @ (database@novox/postgres) are findings.
|
|
//
|
|
// A module may declare its hardware model's word in `particulars`, as a line `model <module> <word>`: the word,
|
|
// one of the names, is then no finding in that module's directory and still one in every other
|
|
// (asus-zephyrus-g14's adopted paths such as /etc/modprobe.d/g14-nvidia-power.conf). A declaration naming no
|
|
// module or no listed name makes the list unreadable.
|
|
// A hardware's own name holding a name is declared as a line `hardware <word>` (asus-g14): that exact word, in
|
|
// any case and not part of a longer hyphened word, is no finding in any module.
|
|
//
|
|
// 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"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// forge is the software's module path on its forge and the name of its record (novox/hq, in any form: a citation,
|
|
// a review, a URL path), blanked before a line is read; any other novox/<x> (a repository in a fixture,
|
|
// testdata/novox/x) is not blanked, nor novox/hqx.
|
|
var forge = regexp.MustCompile(`(?i:git\.novox\.be/novox/[\w.-]+)|novox/hq(?:$|[^\w-])`)
|
|
|
|
// ownNames are the hardware model's and the software's own names, blanked before a line is read: the model
|
|
// in any separator and case (asus-zephyrus-g14, ASUS_ZEPHYRUS_G14, ROG Zephyrus G14), the software as two
|
|
// words or hyphened, whole (Novox Mesh, novox mesh, novox-mesh), so novox-mesh-vault stays a node name joined;
|
|
// and the software's npm scope (@novox/mesh-sdk, @novox:registry=), its package path as the Go module path is,
|
|
// where no word stands before the @, so a session's who@node (fix-1@novox) is still a node name.
|
|
var ownNames = regexp.MustCompile(`(?i)(?:asus[-_ ])?zephyrus[-_ ]g14|(?:^|[^\w-])novox[- ]mesh(?:$|[^\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 {
|
|
entries, err := readList(filepath.Join(root, "particulars"))
|
|
names, models, hardware := splitModels(entries)
|
|
if err == nil && len(names) == 0 {
|
|
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
|
|
}
|
|
var modules []string
|
|
if err == nil {
|
|
modules, err = moduleDirs(root)
|
|
if err == nil && len(modules) == 0 {
|
|
err = fmt.Errorf("modules/ holds no module — the check would pass on anything")
|
|
}
|
|
}
|
|
if err == nil {
|
|
err = checkModels(models, names, modules)
|
|
}
|
|
var blanks *regexp.Regexp
|
|
if err == nil {
|
|
blanks, err = hardwareWords(hardware)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "particulars:", err)
|
|
return 2
|
|
}
|
|
findings, err := run(root, names, modules, models, blanks)
|
|
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 modules\n", len(names), len(modules))
|
|
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()
|
|
}
|
|
|
|
// splitModels parts the list's entries into the names, the models' words by module (`model <module> <word>`)
|
|
// and the hardware's words (`hardware <word>`, kept whole to be checked).
|
|
func splitModels(entries []string) (names []string, models map[string][]string, hardware []string) {
|
|
models = map[string][]string{}
|
|
for _, e := range entries {
|
|
f := strings.Fields(e)
|
|
if len(f) > 0 && f[0] == "hardware" {
|
|
hardware = append(hardware, e)
|
|
continue
|
|
}
|
|
if len(f) > 0 && f[0] == "model" {
|
|
if len(f) != 3 {
|
|
models[""] = append(models[""], e)
|
|
continue
|
|
}
|
|
models[f[1]] = append(models[f[1]], f[2])
|
|
continue
|
|
}
|
|
names = append(names, e)
|
|
}
|
|
return names, models, hardware
|
|
}
|
|
|
|
// hardwareWords is the matcher of the declared hardware words, nil when none is declared; a line that is not
|
|
// `hardware <word>` makes the list unreadable.
|
|
func hardwareWords(lines []string) (*regexp.Regexp, error) {
|
|
var words []string
|
|
for _, l := range lines {
|
|
f := strings.Fields(l)
|
|
if len(f) != 2 {
|
|
return nil, fmt.Errorf("particulars: %q is not `hardware <word>`", l)
|
|
}
|
|
words = append(words, regexp.QuoteMeta(f[1]))
|
|
}
|
|
if len(words) == 0 {
|
|
return nil, nil
|
|
}
|
|
return regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9-])(?:` + strings.Join(words, "|") + `)(?:$|[^A-Za-z0-9-])`), nil
|
|
}
|
|
|
|
// blankWith blanks what rx matches in s, keeping every other byte where it was.
|
|
func blankWith(rx *regexp.Regexp, s string) string {
|
|
if rx == nil {
|
|
return s
|
|
}
|
|
return rx.ReplaceAllStringFunc(s, func(m string) string { return strings.Repeat(" ", len(m)) })
|
|
}
|
|
|
|
// checkModels refuses a model declaration that is malformed, names no module or names a word that is no name.
|
|
func checkModels(models map[string][]string, names, modules []string) error {
|
|
for m, words := range models {
|
|
if m == "" {
|
|
return fmt.Errorf("particulars: %q is not `model <module> <word>`", words[0])
|
|
}
|
|
if !slices.Contains(modules, m) {
|
|
return fmt.Errorf("particulars: model %s %s names no module under modules/", m, words[0])
|
|
}
|
|
for _, w := range words {
|
|
if !slices.ContainsFunc(names, func(n string) bool { return strings.EqualFold(n, w) }) {
|
|
return fmt.Errorf("particulars: model %s %s: %q is not a listed name", m, w, w)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// without returns the names but the words, in any case.
|
|
func without(names, words []string) []string {
|
|
var out []string
|
|
for _, n := range names {
|
|
if !slices.ContainsFunc(words, func(w string) bool { return strings.EqualFold(n, w) }) {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// moduleDirs returns every directory under the catalogue's modules/, sorted.
|
|
func moduleDirs(root string) ([]string, error) {
|
|
entries, err := os.ReadDir(filepath.Join(root, "modules"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
out = append(out, e.Name())
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// run reads every text file of each module named and returns the findings, sorted; a module's model words are
|
|
// not read as names in it.
|
|
func run(root string, all, modules []string, models map[string][]string, hardware *regexp.Regexp) ([]string, error) {
|
|
var out []string
|
|
for _, m := range modules {
|
|
names := without(all, models[m])
|
|
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(blankWith(hardware, 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), blankWith(hardware, 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 and the hardware's and the
|
|
// software's own names blanked first.
|
|
func hits(line string, names []string) []string {
|
|
blank := func(s string) string { return strings.Repeat(" ", len(s)) }
|
|
line = forge.ReplaceAllStringFunc(line, blank)
|
|
line = ownNames.ReplaceAllStringFunc(line, blank)
|
|
var out []string
|
|
for _, n := range names {
|
|
if matcher(n).MatchString(line) {
|
|
out = append(out, strings.ToLower(n))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// matchers holds each name's matcher, compiled once: the check reads every line of every module.
|
|
var matchers = map[string]*regexp.Regexp{}
|
|
|
|
// matcher is the matcher of one name: as a word, or as a part of a CamelCase identifier.
|
|
func matcher(n string) *regexp.Regexp {
|
|
if rx, ok := matchers[n]; ok {
|
|
return rx
|
|
}
|
|
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]`)
|
|
matchers[n] = rx
|
|
return rx
|
|
}
|