code-moniker-check 0.7.1

Rules engine for code-moniker: DSL, rule config/profiles, evaluation over the symbol graph, and suppression.
Documentation
use std::collections::HashMap;

use crate::check::expr::{
	AggregateKind, Atom, Binding, CollectionExpr, CollectionOp, Domain, DomainValueExpr,
	NumberExpr, Op, PairProjection, PairSide, Rhs, ValueExpr,
};
use code_moniker_core::core::moniker::Moniker;

use super::AtomOutcome;

/// Value-vs-Value comparison for the cases where the RHS is itself a
/// projection. Restricted to the ops that pair naturally (equality and
/// numeric ordering); a structural moniker op against a string projection
/// stays `NotApplicable`.
pub(super) fn apply_op_values(lhs: &Value, op: Op, rhs: &Value) -> AtomOutcome {
	use Op::*;
	let ok = match (lhs, op, rhs) {
		(Value::Str(a), Eq, Value::Str(b)) => a == b,
		(Value::Str(a), Ne, Value::Str(b)) => a != b,
		(Value::Number(a), Eq, Value::Number(b)) => a == b,
		(Value::Number(a), Ne, Value::Number(b)) => a != b,
		(Value::Number(a), Lt, Value::Number(b)) => a < b,
		(Value::Number(a), Le, Value::Number(b)) => a <= b,
		(Value::Number(a), Gt, Value::Number(b)) => a > b,
		(Value::Number(a), Ge, Value::Number(b)) => a >= b,
		(Value::Moniker(a), Eq, Value::Moniker(b)) => a == b,
		(Value::Moniker(a), Ne, Value::Moniker(b)) => a != b,
		(Value::Moniker(a), AncestorOf, Value::Moniker(b)) => a.is_ancestor_of(b),
		(Value::Moniker(a), DescendantOf, Value::Moniker(b)) => b.is_ancestor_of(a),
		(Value::Moniker(a), BindMatch, Value::Moniker(b)) => a.bind_match(b),
		_ => return AtomOutcome::NotApplicable,
	};
	if ok {
		AtomOutcome::Pass
	} else {
		AtomOutcome::Fail {
			actual: render_value(lhs),
			expected: render_value(rhs),
		}
	}
}

#[derive(Clone)]
pub(super) enum Value {
	Str(String),
	Number(f64),
	Moniker(Moniker),
}

pub(super) fn apply_op(value: &Value, atom: &Atom) -> AtomOutcome {
	use Op::*;
	let ok = match (value, atom.op, &atom.rhs) {
		(Value::Str(s), RegexMatch, Rhs::RegexStr(_)) => {
			atom.regex.as_ref().is_some_and(|re| re.is_match(s))
		}
		(Value::Str(s), RegexNoMatch, Rhs::RegexStr(_)) => {
			atom.regex.as_ref().is_some_and(|re| !re.is_match(s))
		}
		(Value::Str(s), Eq, Rhs::Str(t)) => s == t,
		(Value::Str(s), Ne, Rhs::Str(t)) => s != t,
		(Value::Moniker(m), Eq, Rhs::Moniker(t)) => m == t,
		(Value::Moniker(m), Ne, Rhs::Moniker(t)) => m != t,
		(Value::Moniker(m), AncestorOf, Rhs::Moniker(t)) => m.is_ancestor_of(t),
		(Value::Moniker(m), DescendantOf, Rhs::Moniker(t)) => t.is_ancestor_of(m),
		(Value::Moniker(m), BindMatch, Rhs::Moniker(t)) => m.bind_match(t),
		(Value::Moniker(m), PathMatch, Rhs::PathPattern(p)) => crate::check::path::matches(p, m),
		_ => return AtomOutcome::NotApplicable,
	};
	if ok {
		AtomOutcome::Pass
	} else {
		let actual = render_value(value);
		let expected = render_rhs(&atom.rhs);
		AtomOutcome::Fail { actual, expected }
	}
}

pub(super) fn render_value(v: &Value) -> String {
	match v {
		Value::Str(s) => s.clone(),
		Value::Number(n) => render_number(*n),
		Value::Moniker(m) => format!("{}b moniker", m.as_encoded().len()),
	}
}

fn render_number(n: f64) -> String {
	if n.fract() == 0.0 {
		format!("{n:.0}")
	} else {
		format!("{n:.3}")
			.trim_end_matches('0')
			.trim_end_matches('.')
			.to_string()
	}
}

fn render_rhs(r: &Rhs) -> String {
	match r {
		Rhs::Str(s) => s.clone(),
		Rhs::Number(expr) => render_number_expr(expr),
		Rhs::RegexStr(s) => s.clone(),
		Rhs::Moniker(m) => format!("{}b moniker", m.as_encoded().len()),
		Rhs::PathPattern(p) => format!("path `{}`", p.raw),
		Rhs::Projection(l) => l.as_str().to_string(),
		Rhs::CurrentProjection(l) => format!("current.{}", l.as_str()),
		Rhs::PairProjection(projection) => pair_projection_label(*projection),
		Rhs::Collection(collection) => collection_label(collection),
	}
}

pub(super) fn number_expr_label(expr: &NumberExpr) -> &'static str {
	match expr {
		NumberExpr::Literal(_) => "number",
		NumberExpr::Projection(lhs) => lhs.as_str(),
		NumberExpr::Count { .. } => "count",
		NumberExpr::Aggregate { kind, .. } => aggregate_label(*kind),
		NumberExpr::Metric { kind, .. } => kind.as_str(),
		NumberExpr::Entropy(_) => "entropy",
		NumberExpr::Size(_) => "size",
	}
}

pub(super) fn render_number_expr(expr: &NumberExpr) -> String {
	match expr {
		NumberExpr::Literal(n) => render_number(*n),
		NumberExpr::Projection(lhs) => lhs.as_str().to_string(),
		NumberExpr::Count { domain, .. } => match domain {
			Domain::Children(kind) => format!("count({kind})"),
			Domain::ChildrenByShape(shape) => format!("count(shape:{shape})"),
			Domain::Descendants(inner) => format!("count(descendants({}))", domain_label(inner)),
			Domain::Pairs(inner) => format!("count(pairs({}))", domain_label(inner)),
			Domain::Segments => "count(segment)".to_string(),
			Domain::OutRefs => "count(out_refs)".to_string(),
			Domain::InRefs => "count(in_refs)".to_string(),
			Domain::SourceOutRefs => "count(source.out_refs)".to_string(),
			Domain::SourceInRefs => "count(source.in_refs)".to_string(),
			Domain::TargetOutRefs => "count(target.out_refs)".to_string(),
			Domain::TargetInRefs => "count(target.in_refs)".to_string(),
			Domain::SourceAncestorOutRefs => "count(source.ancestors.out_refs)".to_string(),
			Domain::SourceAncestorInRefs => "count(source.ancestors.in_refs)".to_string(),
		},
		NumberExpr::Aggregate { kind, domain, .. } => {
			format!("{}({})", aggregate_label(*kind), domain_label(domain))
		}
		NumberExpr::Metric { kind, binding } => {
			format!("{}({})", kind.as_str(), binding_label(*binding))
		}
		NumberExpr::Entropy(collection) => format!("entropy({})", domain_value_label(collection)),
		NumberExpr::Size(collection) => format!("size({})", collection_label(collection)),
	}
}

fn aggregate_label(kind: AggregateKind) -> &'static str {
	kind.as_str()
}

fn binding_label(binding: Binding) -> &'static str {
	match binding {
		Binding::Self_ => "self",
		Binding::Each => "each",
	}
}

fn domain_value_label(collection: &DomainValueExpr) -> String {
	match collection.expr.as_ref() {
		ValueExpr::Item => domain_label(&collection.domain),
		ValueExpr::Projection(lhs) => {
			format!("{}, {}", domain_label(&collection.domain), lhs.as_str())
		}
		ValueExpr::Number(expr) => {
			format!(
				"{}, {}",
				domain_label(&collection.domain),
				render_number_expr(expr)
			)
		}
	}
}

fn domain_label(domain: &Domain) -> String {
	match domain {
		Domain::Children(kind) => kind.clone(),
		Domain::ChildrenByShape(shape) => format!("shape:{shape}"),
		Domain::Descendants(inner) => format!("descendants({})", domain_label(inner)),
		Domain::Pairs(inner) => format!("pairs({})", domain_label(inner)),
		Domain::Segments => "segment".to_string(),
		Domain::OutRefs => "out_refs".to_string(),
		Domain::InRefs => "in_refs".to_string(),
		Domain::SourceOutRefs => "source.out_refs".to_string(),
		Domain::SourceInRefs => "source.in_refs".to_string(),
		Domain::TargetOutRefs => "target.out_refs".to_string(),
		Domain::TargetInRefs => "target.in_refs".to_string(),
		Domain::SourceAncestorOutRefs => "source.ancestors.out_refs".to_string(),
		Domain::SourceAncestorInRefs => "source.ancestors.in_refs".to_string(),
	}
}

pub(super) fn pair_projection_label(projection: PairProjection) -> String {
	let side = match projection.side {
		PairSide::A => "a",
		PairSide::B => "b",
	};
	format!("{side}.{}", projection.lhs.as_str())
}

fn collection_label(collection: &CollectionExpr) -> String {
	match collection {
		CollectionExpr::Projection(projection) => {
			if projection.path.is_empty() {
				domain_label(&projection.domain)
			} else {
				format!(
					"{}.{}",
					domain_label(&projection.domain),
					projection.path.join(".")
				)
			}
		}
		CollectionExpr::PairProjection(projection) => {
			let side = match projection.side {
				PairSide::A => "a",
				PairSide::B => "b",
			};
			if projection.path.is_empty() {
				format!("{side}.{}", domain_label(&projection.domain))
			} else {
				format!(
					"{side}.{}.{}",
					domain_label(&projection.domain),
					projection.path.join(".")
				)
			}
		}
		CollectionExpr::Unique(inner) => format!("unique({})", collection_label(inner)),
		CollectionExpr::Binary { op, left, right } => {
			let op = match op {
				CollectionOp::Intersect => "intersect",
				CollectionOp::Union => "union",
				CollectionOp::Difference => "diff",
			};
			format!(
				"{} {op} {}",
				collection_label(left),
				collection_label(right)
			)
		}
	}
}

#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(super) enum ValueKey {
	Str(String),
	Number(u64),
	Moniker(Moniker),
}

impl ValueKey {
	pub(super) fn from_value(value: Value) -> Self {
		match value {
			Value::Str(s) => Self::Str(s),
			Value::Number(n) => Self::Number(n.to_bits()),
			Value::Moniker(m) => Self::Moniker(m),
		}
	}

	pub(super) fn into_value(self) -> Value {
		match self {
			Self::Str(s) => Value::Str(s),
			Self::Number(bits) => Value::Number(f64::from_bits(bits)),
			Self::Moniker(moniker) => Value::Moniker(moniker),
		}
	}
}

pub(super) fn mode_value(values: Vec<Value>) -> Option<Value> {
	let mut counts = HashMap::new();
	let mut order = Vec::new();
	for value in values {
		let key = ValueKey::from_value(value);
		if !counts.contains_key(&key) {
			order.push(key.clone());
		}
		*counts.entry(key).or_insert(0) += 1;
	}
	let mut best = None;
	for key in order {
		let count = counts.get(&key).copied().unwrap_or(0);
		if best
			.as_ref()
			.is_none_or(|(_, best_count)| count > *best_count)
		{
			best = Some((key, count));
		}
	}
	best.map(|(key, _)| key.into_value())
}

pub(super) fn value_counts(values: impl Iterator<Item = Value>) -> HashMap<ValueKey, usize> {
	let mut counts = HashMap::new();
	for value in values {
		*counts.entry(ValueKey::from_value(value)).or_insert(0) += 1;
	}
	counts
}