// 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/) are blanked // before a line is read. // // 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.-]+`) 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 }