From 96b7b6551ce64083241f0219b7daa1a6f80c1a70 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 21:32:13 +0200 Subject: [PATCH] Tell the software's npm scope from a node in the particulars check, and compile its matchers once (issue 1838) @novox/mesh-sdk and @novox:registry= are the software's package path, as the Go module path is; who@node is still a node name (1838/AC2). Reading every module, a regexp compiled per name per line made the check take 41 s; compiled once, 7.5 s. --- checks/particulars/main.go | 28 +++++++++++++++++++++------- checks/particulars/main_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/checks/particulars/main.go b/checks/particulars/main.go index 48b75108..6f0c745b 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -8,7 +8,7 @@ // 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 (Novox Mesh, novox-mesh). +// and the software's own name and npm scope (Novox Mesh, novox-mesh, @novox/mesh-sdk). // // go run . // @@ -33,8 +33,10 @@ var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\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. -var ownNames = regexp.MustCompile(`(?i)(?:asus[-_ ])?zephyrus[-_ ]g14|(?:^|[^\w-])novox[- ]mesh(?:$|[^\w-])`) +// 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 := "." @@ -173,12 +175,24 @@ func hits(line string, names []string) []string { line = ownNames.ReplaceAllStringFunc(line, blank) 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) { + 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 +} diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go index ac1c970c..faf8914d 100644 --- a/checks/particulars/main_test.go +++ b/checks/particulars/main_test.go @@ -169,3 +169,32 @@ func TestANodeNameBesideTheHardwareOrTheSoftwareIsRefused(t *testing.T) { } } } + +// 1838/AC2: the software's npm scope (@novox/mesh-sdk, @novox:registry=) is its package path, as the Go module +// path is; a session's who@node naming a node is still a node name. +func TestTheSoftwaresNpmScopeIsNotANodeName(t *testing.T) { + for _, s := range []string{ + "printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)}", + "const url = `${asked}@novox%2fmesh-sdk`;", + "# The @novox scope resolves from the mesh's own package registry", + `"dependencies": { "@novox/mesh-sdk": "^0.1.0" }`, + } { + if got := scan("f", s, names); len(got) != 0 { + t.Errorf("%q refused: %v", s, got) + } + } + for _, s := range []string{"by fix-1@novox", "jochen@novox:"} { + if got := scan("f", s, names); len(got) != 1 { + t.Errorf("%q gave %v, want one finding", s, got) + } + } +} + +// 1838/AC1: reading every module, the check compiles its matchers once, not once a line: a line read costs a +// few allocations, not a regexp compiled per name (the whole catalogue took 41 s with one per line). +func TestALineIsReadWithoutCompilingAMatcher(t *testing.T) { + line := "a line of code that names nothing at all, with git.novox.be/novox/mesh-sdk in it" + if n := testing.AllocsPerRun(100, func() { hits(line, names) }); n > 20 { + t.Errorf("a line costs %.0f allocations, want at most 20", n) + } +}