From 24a84671ace373ae331fa83a1ff484990f4dff0e Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 12 Sep 2026 12:58:14 +0200 Subject: krino: planning — chains, placeholders, conflicts, JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cond/compile.go | 22 ++++++++++++++++++++++ internal/cond/compile_test.go | 17 +++++++++++++++++ internal/cond/types.go | 12 ++++++++++++ 3 files changed, 51 insertions(+) (limited to 'internal/cond') diff --git a/internal/cond/compile.go b/internal/cond/compile.go index 4fdc75f..590d61a 100644 --- a/internal/cond/compile.go +++ b/internal/cond/compile.go @@ -362,6 +362,28 @@ func (c *compiler) compileMatched(n *sexp.Node) *node { return &node{kind: kMatched, pos: n.Pos, label: "matched", cost: costCheap} } +// collectNameGroups walks n and its children — and and or included, not +// excluded — appending the capture-group count of every pattern of every +// kName node reachable without crossing a not, in compile order. B1: a +// name test under a not never supplies Result.Captures (eval.go's negated +// tracking takes captures only when !negated), so it must not count toward +// checkCaptures' "does some name test in this rule have enough groups" +// either - a rule combining a capturing name test with an unrelated +// (not (name ...)) must still pass. +func collectNameGroups(n *node, out *[]int) { + if n == nil || n.kind == kNot { + return + } + if n.kind == kName { + for _, p := range n.patterns { + *out = append(*out, p.re.NumSubexp()) + } + } + for _, ch := range n.children { + collectNameGroups(ch, out) + } +} + // quotedExampleErr records the shared "takes X in quotes: write (test // "arg")" diagnostic used by name, path, content and duplicate. func (c *compiler) quotedExampleErr(a *sexp.Node, test, noun string) { diff --git a/internal/cond/compile_test.go b/internal/cond/compile_test.go index e68a4d8..2a07ad6 100644 --- a/internal/cond/compile_test.go +++ b/internal/cond/compile_test.go @@ -124,6 +124,23 @@ func TestEmptyChildrenGuard(t *testing.T) { } } +// TestNameGroups: B1b - NameGroups() reports the capture-group count of +// every name test reachable without crossing a not, and skips one reachable +// only under a not (B1): here the outer (name ...) has two groups and the +// (not (name ...)) one has one, but the result must carry only the outer's +// count. +func TestNameGroups(t *testing.T) { + c, errs := Compile("d.conf", nodes(t, `(and (name "inv-(\d+)-(\d+)") (not (name "draft-(\d+)")))`), Options{}) + if len(errs) > 0 { + t.Fatal(errs) + } + got := c.NameGroups() + want := []int{2} + if !reflect.DeepEqual(got, want) { + t.Errorf("NameGroups() = %v, want %v", got, want) + } +} + func TestGroupsMatchSpec(t *testing.T) { want := map[string]string{ "image": "jpg jpeg png gif webp bmp tif tiff heic heif avif svg ico raw cr2 nef arw dng", diff --git a/internal/cond/types.go b/internal/cond/types.go index 81c2f45..9455f63 100644 --- a/internal/cond/types.go +++ b/internal/cond/types.go @@ -93,6 +93,18 @@ type node struct { dirs []string } +// NameGroups returns the number of capture groups of every name test in the +// condition that can ever supply captures, in compile order: a name test +// nested inside and/or counts however deep, but one inside a not does not +// (B1) - it can never be the source of Result.Captures, so it must not be +// asked to justify a rule's use of {N} either. Empty when the rule has no +// such name test. +func (c *Cond) NameGroups() []int { + var out []int + collectNameGroups(c.root, &out) + return out +} + // groups maps a (type ...) group name to the extensions it expands to, // spec Appendix A. var groups = map[string][]string{ -- cgit v1.3