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. */