proj/portage:master commit in: lib/portage/tests/dep/, lib/portage/dep/, src/

"Matt Turner" <[email protected]>
Newsgroups gmane.linux.gentoo.cvs
Message-ID <1785963928.c79b938fac146e690e046beab0e13a632e1bd0a6.mattst88@gentoo>
commit:     c79b938fac146e690e046beab0e13a632e1bd0a6
Author:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
AuthorDate: Sun Aug  2 20:18:53 2026 +0000
Commit:     Matt Turner <mattst88 <AT> gentoo <DOT> org>
CommitDate: Wed Aug  5 21:05:28 2026 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=c79b938f

dep: preserve conjunctions inside any-of groups

The C dep-string parser inlined naked all-of groups and active
USE-conditional groups directly into their parent.  In an all-of context
that is correct, but inside an any-of (||) group it silently drops the
conjunction: "|| ( ( a b ) c )" yields ["||", ["a", "b", "c"]] instead
of ["||", [["a", "b"], "c"]].

Fix dep_parser_core.c to call on_group_start/on_group_end with an empty
operator for naked all-of groups and for active conditional groups,
rather than inlining their contents, and fix dep_parser.c
(py_on_group_end) to append only the sublist when the operator is empty,
preserving the conjunction as a nested list without emitting a spurious
empty string.

The raw parse tree then carries explicit all-of group nesting, which is
not the shape use_reduce returns, so add _c_normalize_seq and
_c_normalize_alts to reduce it to use_reduce's non-flat form: inline
all-of groups in an all-of context, keep conjunctions nested as any-of
alternatives, flatten nested any-of, unwrap single-alternative any-of,
collapse empty any-of (dropped pre-EAPI 7, placeholder atom in EAPI 7+),
and suppress redundant-bracket removal under an any-of.

Validated by a differential fuzz of ~360k combinations across dep
strings, USE lists, EAPIs, and flat/non-flat modes over randomly
generated nested dep trees (depth up to 5): no mismatches against the
pure-Python path.  Adds regression tests for the conjunction-in-any-of
cases, and for nesting depth across the point where the C parser's
group stack spills to the heap.

Signed-off-by: Matt Turner <mattst88 <AT> gentoo.org>

 lib/portage/dep/__init__.py            | 73 +++++++++++++++++++++++++++++++++-
 lib/portage/tests/dep/test_c_parser.py | 48 ++++++++++++++++++++++
 src/dep_parser.c                       | 17 +++++---
 src/dep_parser_core.c                  | 20 +++++++---
 src/dep_parser_core.h                  |  4 +-
 5 files changed, 148 insertions(+), 14 deletions(-)

diff --git a/lib/portage/dep/__init__.py b/lib/portage/dep/__init__.py
index 48b41849c..07c270e65 100644
--- a/lib/portage/dep/__init__.py
+++ b/lib/portage/dep/__init__.py
@@ -130,10 +130,81 @@ def _c_convert_result(items, eapi, eapi_attrs, uselist, matchall):
     return result
 
 
+def _c_normalize_alts(group, empty_always_true):
+    """Reduce the alternatives of one || group to use_reduce's non-flat form."""
+    alts = []
+    i = 0
+    n = len(group)
+    while i < n:
+        item = group[i]
+        if isinstance(item, str):  # a nested '||'
+            i += 1
+            nested = _c_normalize_alts(group[i], empty_always_true)
+            if nested:
+                alts.extend(nested)
+            elif not empty_always_true:
+                # empty nested || -> placeholder atom (EAPI 7+)
+                alts.append([Atom("__const__/empty-any-of")])
+        elif isinstance(item, list):
+            # conjunction: normalize under_anyof to suppress bracket removal
+            sub = _c_normalize_seq(item, empty_always_true, under_anyof=True)
+            if len(sub) == 1:
+                # ( X ) alternative -> X
+                alts.append(sub[0])
+            elif len(sub) == 2 and sub[0] == "||":
+                # ( || ( ... ) ) alternative -> its alternatives flatten into
+                # the enclosing any-of.
+                alts.extend(sub[1])
+            elif sub:
+                alts.append(sub)
+        else:
+            alts.append(item)
+        i += 1
+    return alts
+
+
+def _c_normalize_seq(seq, empty_always_true, under_anyof=False):
+    """Reduce a C parse (sub)tree to use_reduce's non-flat form.
+
+    under_anyof: this sequence is an alternative of an enclosing || group;
+    use_reduce keeps redundant brackets there, suppressing conjunction inlining."""
+    out = []
+    i = 0
+    n = len(seq)
+    while i < n:
+        item = seq[i]
+        if isinstance(item, str):  # '||'
+            i += 1
+            alts = _c_normalize_alts(seq[i], empty_always_true)
+            if not alts:
+                # || ( ) -> dropped (EAPI < 7) or a const placeholder (EAPI 7+).
+                if not empty_always_true:
+                    out.append(Atom("__const__/empty-any-of"))
+            elif len(alts) == 1:
+                # || ( X ) -> X
+                alt = alts[0]
+                if isinstance(alt, list) and not under_anyof:
+                    out.extend(alt)
+                else:
+                    out.append(alt)
+            else:
+                out.append("||")
+                out.append(alts)
+        elif isinstance(item, list):
+            out.extend(
+                _c_normalize_seq(item, empty_always_true, under_anyof=under_anyof)
+            )
+        else:
+            out.append(item)
+        i += 1
+    return out
+
+
 def _c_fast_use_reduce(depstr, uselist, matchall, eapi):
     raw = _c_dep_parser.parse(depstr, uselist=uselist, matchall=matchall)
     eapi_attrs = _get_eapi_attrs(eapi)
-    return _c_convert_result(raw, eapi, eapi_attrs, uselist, matchall)
+    result = _c_convert_result(raw, eapi, eapi_attrs, uselist, matchall)
+    return _c_normalize_seq(result, eapi_attrs.empty_groups_always_true)
 
 
 # \w is [a-zA-Z0-9_]

diff --git a/lib/portage/tests/dep/test_c_parser.py b/lib/portage/tests/dep/test_c_parser.py
index d845f4367..765890452 100644
--- a/lib/portage/tests/dep/test_c_parser.py
+++ b/lib/portage/tests/dep/test_c_parser.py
@@ -719,6 +719,45 @@ class TestCFastVsPython(TestCase):
     def test_slot_operator_bare_eq(self):
         self._compare("dev-libs/foo:=", matchall=True)
 
+    def test_anyof_conjunction_alternative(self):
+        # ( a b ) inside || is a conjunction and must stay nested, not flattened.
+        self._compare("|| ( ( dev-libs/a dev-libs/b ) dev-libs/c )", matchall=True)
+
+    def test_anyof_conjunction_second(self):
+        self._compare("|| ( dev-libs/a ( dev-libs/b dev-libs/c ) )", matchall=True)
+
+    def test_anyof_two_conjunctions(self):
+        self._compare(
+            "|| ( ( dev-libs/a dev-libs/b ) ( dev-libs/c dev-libs/d ) )",
+            matchall=True,
+        )
+
+    def test_anyof_active_conditional_conjunction(self):
+        # An active USE-conditional group inside || is also a conjunction.
+        self._compare(
+            "|| ( foo? ( dev-libs/a dev-libs/b ) dev-libs/c )", uselist=["foo"]
+        )
+
+    def test_anyof_nested_anyof_flattens(self):
+        self._compare("|| ( dev-libs/a || ( dev-libs/b dev-libs/c ) )", matchall=True)
+
+    def test_anyof_naked_wrapping_anyof(self):
+        # ( || ( b c ) ) inside || flattens its alternatives up.
+        self._compare(
+            "|| ( ( || ( dev-libs/b dev-libs/c ) ) dev-libs/a )", matchall=True
+        )
+
+    def test_anyof_deeply_nested_conjunctions(self):
+        self._compare(
+            "|| ( ( dev-libs/a ( dev-libs/b dev-libs/c ) ) dev-libs/e )",
+            matchall=True,
+        )
+
+    def test_anyof_empty_conditional_eapi7(self):
+        # Empty || after USE evaluation yields a placeholder atom in EAPI 7+.
+        self._compare("|| ( foo? ( dev-libs/a ) )", uselist=[], eapi="8")
+        self._compare("|| ( foo? ( dev-libs/a ) )", uselist=[], eapi="6")
+
     def test_invalid_raises_same_exception(self):
         bad_cases = [
             "|| ( dev-libs/a dev-libs/b",
@@ -758,6 +797,15 @@ class TestDeepNesting(TestCase):
                     self._reduce(depstr, use_c=False),
                 )
 
+    def test_nesting_parity_any_of(self):
+        for depth in (1, 8, 31, 32, 33, 64, 65, 200):
+            with self.subTest(depth=depth):
+                depstr = "|| ( " * depth + "dev-libs/a" + " )" * depth
+                self.assertEqual(
+                    self._reduce(depstr, use_c=True),
+                    self._reduce(depstr, use_c=False),
+                )
+
 
 class TestLongAtoms(TestCase):
     """Category and package names are not length-limited, so the C path must

diff --git a/src/dep_parser.c b/src/dep_parser.c
index 8940f83b3..95edd2ec8 100644
--- a/src/dep_parser.c
+++ b/src/dep_parser.c
@@ -377,14 +377,19 @@ static int py_on_group_end(void *vctx)
     PyObject *sublist  = ctx->frames[ctx->depth].list;
     PyObject *parent   = ctx->frames[ctx->depth - 1].list;
 
-    int rc = PyList_Append(parent, group_op);
-    Py_DECREF(group_op);
-    if (rc < 0) {
-        Py_DECREF(sublist);
-        return 0;
+    /* Empty operator = naked all-of: append only the sublist. */
+    if (PyUnicode_GET_LENGTH(group_op) != 0) {
+        int rc = PyList_Append(parent, group_op);
+        Py_DECREF(group_op);
+        if (rc < 0) {
+            Py_DECREF(sublist);
+            return 0;
+        }
+    } else {
+        Py_DECREF(group_op);
     }
 
-    rc = PyList_Append(parent, sublist);
+    int rc = PyList_Append(parent, sublist);
     Py_DECREF(sublist);
     return rc >= 0;
 }

diff --git a/src/dep_parser_core.c b/src/dep_parser_core.c
index 6e8b237ae..836b711e9 100644
--- a/src/dep_parser_core.c
+++ b/src/dep_parser_core.c
@@ -517,7 +517,8 @@ static int scan_item(DepScanner *p, DepVisitor *v)
 
     const char *s = p->cur;
 
-    /* naked group: ( items ) - all-of, inline */
+    /* Plain "( items )": reported as a group with an empty operator rather
+     * than inlined, so a conjunction inside an any-of keeps its nesting. */
     if (s < p->end && *s == '(') {
         p->cur = s + 1;
         if (p->cur >= p->end || !is_whitespace(*p->cur)) {
@@ -526,11 +527,14 @@ static int scan_item(DepScanner *p, DepVisitor *v)
         }
         skip_whitespace(p);
 
+        if (!v->on_group_start(v->ctx, "", 0))
+            return 0;
+
         if (!scan_group_contents(p, v))
             return 0;
 
         p->cur++;  /* consume ')' */
-        return 1;
+        return v->on_group_end(v->ctx);
     }
 
     int         is_group_op = 0;
@@ -602,16 +606,20 @@ static int scan_item(DepScanner *p, DepVisitor *v)
             return 0;
 
         if (active) {
-            if (!scan_group_contents(p, v)) {
+            /* active conditional: emit as naked all-of to preserve nesting */
+            if (!v->on_group_start(v->ctx, "", 0))
                 return 0;
-            }
+            if (!scan_group_contents(p, v))
+                return 0;
+            p->cur++;  /* consume ')' */
+            return v->on_group_end(v->ctx);
         } else {
             if (!scan_group_contents(p, &skip_visitor)) {
                 return 0;
             }
+            p->cur++;  /* consume ')' */
+            return 1;
         }
-        p->cur++;  /* consume ')' */
-        return 1;
     }
 }
 

diff --git a/src/dep_parser_core.h b/src/dep_parser_core.h
index 25af21518..e81e692c6 100644
--- a/src/dep_parser_core.h
+++ b/src/dep_parser_core.h
@@ -73,7 +73,9 @@ typedef struct {
     void *ctx;
     /* Called for each atom token. Returns 1 on success, 0 on error. */
     int (*on_atom)(void *ctx, const char *start, int len, const AtomInfo *info);
-    /* Called before/after || ^^ ?? group contents. */
+    /* Called before/after group contents.  op is "||", "^^" or "??";
+     * an empty op means a plain all-of group, which nests but has no
+     * operator token of its own. */
     int (*on_group_start)(void *ctx, const char *op, int op_len);
     int (*on_group_end)(void *ctx);
     /* Returns 1 if the use flag is active, 0 if not, -1 on error. */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.