// 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. // Three names hold a node name and are not one, so they are blanked before a line is read: the forge's host // (git.novox.be) and organisation path (novox/), 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). // // go run . // // 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.-]+`) // 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 { 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 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 { fmt.Fprintln(os.Stderr, "particulars:", err) return 2 } findings, err := run(root, names, modules) 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() } // 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. func run(root string, names, modules []string) ([]string, error) { var out []string for _, m := range modules { 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 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 }