From a3b51a9d9a024dc5838249c1621e2008ef3fa22e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 14:18:26 +0200 Subject: [PATCH] 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. --- checks/particulars/main.go | 59 ++++++++++++++++++++------------- checks/particulars/main_test.go | 59 ++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 27 deletions(-) diff --git a/checks/particulars/main.go b/checks/particulars/main.go index f20862b2..5203f1f6 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -3,8 +3,8 @@ // 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` beside this file, one a line. A name is matched -// whole and in any case; a hyphen joins a word, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word. +// 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. // @@ -25,41 +25,49 @@ import ( "strings" ) -var forge = regexp.MustCompile(`(?i)git\.novox\.be|\bnovox/[\w.-]+`) +// 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("covered") + 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) - os.Exit(2) + return 2 } findings, err := run(root, names, covered) if err != nil { fmt.Fprintln(os.Stderr, "particulars:", err) - os.Exit(2) + 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)) - os.Exit(1) + 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. @@ -99,7 +107,9 @@ func run(root string, names, covered []string) ([]string, error) { } rel, _ := filepath.Rel(root, path) // A path is read too: a testdata directory named after a node is a finding. - out = append(out, scan(rel+" (name)", filepath.ToSlash(rel), names)...) + 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 } @@ -117,9 +127,6 @@ func run(root string, names, covered []string) ([]string, error) { return nil, err } } - for i, f := range out { - out[i] = strings.Replace(f, " (name):", ":0:", 1) - } sort.Strings(out) return out, nil } @@ -127,19 +134,25 @@ func run(root string, names, covered []string) ([]string, error) { // 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 - var rxs []*regexp.Regexp - for _, n := range names { - // Whole word, underscore a separator, hyphen a joiner; and a CamelCase part of an identifier (TestAce). - q := regexp.QuoteMeta(n) - camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:]) - rxs = append(rxs, regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9-])`+q+`(?:$|[^A-Za-z0-9-]))|[a-z0-9]`+camel+`(?:$|[^a-z])`)) - } for i, line := range strings.Split(src, "\n") { - line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) }) - for j, rx := range rxs { - if rx.MatchString(line) { - out = append(out, fmt.Sprintf("%s:%d: node name %q — %s", file, i+1, strings.ToLower(names[j]), strings.Join(strings.Fields(line), " "))) - } + 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 diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go index 6312cf36..9522c00f 100644 --- a/checks/particulars/main_test.go +++ b/checks/particulars/main_test.go @@ -35,14 +35,65 @@ func TestTheForgeIsNotANodeName(t *testing.T) { "see novox/hq issue 5", "module git.novox.be/novox/mesh-sdk/go v1.2.3", "https://git.novox.be/novox/mesh-catalog", - "space, trace, surface, grace, ace-of, mesh-novox, g14x", + "git.novox.be", + "space trace surface grace g14x", } { - if got := scan("f", s, names); len(got) != 0 && !strings.Contains(s, "ace-of") { + if got := scan("f", s, names); len(got) != 0 { t.Errorf("%q refused: %v", s, got) } } - if got := scan("f", "space trace surface grace g14x", names); len(got) != 0 { - t.Errorf("longer words refused: %v", got) +} + +func TestANodeNameInAPathOrJoinedIsRefused(t *testing.T) { + for _, s := range []string{ + "modules/kernel/testdata/novox/meminfo", + "/srv/novox/data", + "testdata/shanks-meminfo.txt", + "on-shanks", + "shanks-desktop", + "mesh-novox", + "func novoxTree() {}", + "aceFiles := 1", + "g14Meminfo := 2", + "novox_tree := 3", + } { + if got := scan("f", s, names); len(got) == 0 { + t.Errorf("%q not refused", s) + } + } +} + +func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) { + root := t.TempDir() + p := filepath.Join(root, "modules/kernel/testdata/ace/x") + os.MkdirAll(filepath.Dir(p), 0o755) + os.WriteFile(p, []byte("clean\n"), 0o644) + got, err := run(root, names, []string{"kernel"}) + if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") { + t.Errorf("got %v, %v", got, err) + } +} + +func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) { + root := t.TempDir() + write := func(p, s string) { + p = filepath.Join(root, p) + os.MkdirAll(filepath.Dir(p), 0o755) + os.WriteFile(p, []byte(s), 0o644) + } + write("particulars", "ace\n") + write("checks/particulars/covered", "kernel\n") + write("modules/kernel/a", "clean\n") + if got := check(root); got != 0 { + t.Errorf("clean = %d, want 0", got) + } + write("modules/kernel/a", "on ace\n") + if got := check(root); got != 1 { + t.Errorf("finding = %d, want 1", got) + } + os.Remove(filepath.Join(root, "particulars")) + if got := check(root); got != 2 { + t.Errorf("no list = %d, want 2", got) } }