use std::fmt;
use std::ops::Bound;
use indexmap::IndexMap;
use itertools::Itertools;
use pep440_rs::{Version, VersionSpecifier};
use rustc_hash::FxBuildHasher;
use version_ranges::Ranges;
use crate::{ExtraOperator, MarkerExpression, MarkerOperator, MarkerTree, MarkerTreeKind};
pub(crate) fn to_dnf(tree: &MarkerTree) -> Vec<Vec<MarkerExpression>> {
let mut dnf = Vec::new();
collect_dnf(tree, &mut dnf, &mut Vec::new());
simplify(&mut dnf);
dnf
}
fn collect_dnf(
tree: &MarkerTree,
dnf: &mut Vec<Vec<MarkerExpression>>,
path: &mut Vec<MarkerExpression>,
) {
match tree.kind() {
MarkerTreeKind::False => {}
MarkerTreeKind::True => {
if !path.is_empty() {
dnf.push(path.clone());
}
}
MarkerTreeKind::Version(marker) => {
for (tree, range) in collect_edges(marker.edges()) {
if let Some(excluded) = range_inequality(&range) {
let current = path.len();
for version in excluded {
path.push(MarkerExpression::Version {
key: marker.key().clone(),
specifier: VersionSpecifier::not_equals_version(version.clone()),
});
}
collect_dnf(&tree, dnf, path);
path.truncate(current);
continue;
}
if let Some(specifier) = star_range_inequality(&range) {
path.push(MarkerExpression::Version {
key: marker.key().clone(),
specifier,
});
collect_dnf(&tree, dnf, path);
path.pop();
continue;
}
for bounds in range.iter() {
let current = path.len();
for specifier in VersionSpecifier::from_release_only_bounds(bounds) {
path.push(MarkerExpression::Version {
key: marker.key().clone(),
specifier,
});
}
collect_dnf(&tree, dnf, path);
path.truncate(current);
}
}
}
MarkerTreeKind::String(marker) => {
for (tree, range) in collect_edges(marker.children()) {
if let Some(excluded) = range_inequality(&range) {
let current = path.len();
for value in excluded {
path.push(MarkerExpression::String {
key: marker.key().clone(),
operator: MarkerOperator::NotEqual,
value: value.clone(),
});
}
collect_dnf(&tree, dnf, path);
path.truncate(current);
continue;
}
for bounds in range.iter() {
let current = path.len();
for (operator, value) in MarkerOperator::from_bounds(bounds) {
path.push(MarkerExpression::String {
key: marker.key().clone(),
operator,
value: value.clone(),
});
}
collect_dnf(&tree, dnf, path);
path.truncate(current);
}
}
}
MarkerTreeKind::In(marker) => {
for (value, tree) in marker.children() {
let operator = if value {
MarkerOperator::In
} else {
MarkerOperator::NotIn
};
let expr = MarkerExpression::String {
key: marker.key().clone(),
value: marker.value().to_owned(),
operator,
};
path.push(expr);
collect_dnf(&tree, dnf, path);
path.pop();
}
}
MarkerTreeKind::Contains(marker) => {
for (value, tree) in marker.children() {
let operator = if value {
MarkerOperator::Contains
} else {
MarkerOperator::NotContains
};
let expr = MarkerExpression::String {
key: marker.key().clone(),
value: marker.value().to_owned(),
operator,
};
path.push(expr);
collect_dnf(&tree, dnf, path);
path.pop();
}
}
MarkerTreeKind::Extra(marker) => {
for (value, tree) in marker.children() {
let operator = if value {
ExtraOperator::Equal
} else {
ExtraOperator::NotEqual
};
let expr = MarkerExpression::Extra {
name: marker.name().clone(),
operator,
};
path.push(expr);
collect_dnf(&tree, dnf, path);
path.pop();
}
}
}
}
fn simplify(dnf: &mut Vec<Vec<MarkerExpression>>) {
for i in 0..dnf.len() {
let clause = &dnf[i];
let mut redundant_terms = Vec::new();
'term: for (skipped, skipped_term) in clause.iter().enumerate() {
for (j, other_clause) in dnf.iter().enumerate() {
if i == j {
continue;
}
if other_clause.iter().all(|term| {
if term == skipped_term {
return false;
}
if is_negation(term, skipped_term) {
return true;
}
clause
.iter()
.position(|x| x == term)
.is_some_and(|i| !redundant_terms.contains(&i))
}) {
redundant_terms.push(skipped);
continue 'term;
}
}
}
redundant_terms.sort_by(|a, b| b.cmp(a));
for term in redundant_terms {
dnf[i].remove(term);
}
}
let mut redundant_clauses = Vec::new();
'clause: for i in 0..dnf.len() {
let clause = &dnf[i];
for (j, other_clause) in dnf.iter().enumerate() {
if i == j || redundant_clauses.contains(&j) {
continue;
}
if other_clause.iter().all(|term| {
clause.contains(term)
}) {
redundant_clauses.push(i);
continue 'clause;
}
}
}
for i in redundant_clauses.into_iter().rev() {
dnf.remove(i);
}
}
pub(crate) fn collect_edges<'a, T>(
map: impl ExactSizeIterator<Item = (&'a Ranges<T>, MarkerTree)>,
) -> IndexMap<MarkerTree, Ranges<T>, FxBuildHasher>
where
T: Ord + Clone + 'a,
{
let mut paths: IndexMap<_, Ranges<_>, FxBuildHasher> = IndexMap::default();
for (range, tree) in map {
let (start, end) = range.bounding_range().unwrap();
let range = Ranges::from_range_bounds((start.cloned(), end.cloned()));
paths
.entry(tree)
.and_modify(|union| *union = union.union(&range))
.or_insert_with(|| range.clone());
}
paths
}
fn range_inequality<T>(range: &Ranges<T>) -> Option<Vec<&T>>
where
T: Ord + Clone + fmt::Debug,
{
if range.is_empty() || range.bounding_range() != Some((Bound::Unbounded, Bound::Unbounded)) {
return None;
}
let mut excluded = Vec::new();
for ((_, end), (start, _)) in range.iter().tuple_windows() {
match (end, start) {
(Bound::Excluded(v1), Bound::Excluded(v2)) if v1 == v2 => excluded.push(v1),
_ => return None,
}
}
Some(excluded)
}
fn star_range_inequality(range: &Ranges<Version>) -> Option<VersionSpecifier> {
let (b1, b2) = range.iter().collect_tuple()?;
match (b1, b2) {
((Bound::Unbounded, Bound::Excluded(v1)), (Bound::Included(v2), Bound::Unbounded))
if v1.release().len() == 2
&& v2.release() == [v1.release()[0], v1.release()[1] + 1] =>
{
Some(VersionSpecifier::not_equals_star_version(v1.clone()))
}
_ => None,
}
}
fn is_negation(left: &MarkerExpression, right: &MarkerExpression) -> bool {
match left {
MarkerExpression::Version { key, specifier } => {
let MarkerExpression::Version {
key: key2,
specifier: specifier2,
} = right
else {
return false;
};
key == key2
&& specifier.version() == specifier2.version()
&& specifier
.operator()
.negate()
.is_some_and(|negated| negated == *specifier2.operator())
}
MarkerExpression::VersionIn {
key,
versions,
negated,
} => {
let MarkerExpression::VersionIn {
key: key2,
versions: versions2,
negated: negated2,
} = right
else {
return false;
};
key == key2 && versions == versions2 && negated != negated2
}
MarkerExpression::String {
key,
operator,
value,
} => {
let MarkerExpression::String {
key: key2,
operator: operator2,
value: value2,
} = right
else {
return false;
};
key == key2
&& value == value2
&& operator
.negate()
.is_some_and(|negated| negated == *operator2)
}
MarkerExpression::Extra { operator, name } => {
let MarkerExpression::Extra {
name: name2,
operator: operator2,
} = right
else {
return false;
};
name == name2 && operator.negate() == *operator2
}
}
}