code-moniker-check 0.6.1

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

use crate::check::expr::{
	CollectionExpr, CollectionOp, CollectionProjection, Domain, Lhs, PairCollectionProjection,
	PairSide,
};

use super::local::{DomainItem, domain_items};
use super::value::{Value, ValueKey, value_counts};
use super::{EvalCtx, resolve_def_lhs, resolve_ref_lhs};

#[derive(Clone, Copy)]
pub(super) struct PairCollectionScope<'a> {
	pub(super) a: DomainItem<'a>,
	pub(super) b: DomainItem<'a>,
	pub(super) def_idx: usize,
}

pub(super) fn eval_collection_size(
	collection: &CollectionExpr,
	def_idx: usize,
	_self_idx: usize,
	ctx: &EvalCtx<'_, '_>,
) -> usize {
	eval_collection(collection, def_idx, ctx).len()
}

pub(super) fn eval_collection_subset(
	left: &CollectionExpr,
	right: &CollectionExpr,
	def_idx: usize,
	_self_idx: usize,
	ctx: &EvalCtx<'_, '_>,
) -> bool {
	let left = eval_collection(left, def_idx, ctx);
	let right = eval_collection(right, def_idx, ctx);
	is_subset(&left, &right)
}

pub(super) fn collection_has_pair_binding(collection: &CollectionExpr) -> bool {
	match collection {
		CollectionExpr::Projection(_) => false,
		CollectionExpr::PairProjection(_) => true,
		CollectionExpr::Unique(inner) => collection_has_pair_binding(inner),
		CollectionExpr::Binary { left, right, .. } => {
			collection_has_pair_binding(left) || collection_has_pair_binding(right)
		}
	}
}

pub(super) fn eval_pair_collection_size(
	collection: &CollectionExpr,
	scope: PairCollectionScope<'_>,
	ctx: &EvalCtx<'_, '_>,
) -> Option<usize> {
	eval_pair_collection(collection, scope, ctx).map(|values| values.len())
}

pub(super) fn eval_pair_collection_subset(
	left: &CollectionExpr,
	right: &CollectionExpr,
	scope: PairCollectionScope<'_>,
	ctx: &EvalCtx<'_, '_>,
) -> Option<bool> {
	let left = eval_pair_collection(left, scope, ctx)?;
	let right = eval_pair_collection(right, scope, ctx)?;
	Some(is_subset(&left, &right))
}

fn eval_collection(
	collection: &CollectionExpr,
	def_idx: usize,
	ctx: &EvalCtx<'_, '_>,
) -> Vec<Value> {
	match collection {
		CollectionExpr::Projection(projection) => collect_projection(projection, def_idx, ctx),
		CollectionExpr::PairProjection(_) => Vec::new(),
		CollectionExpr::Unique(inner) => unique(eval_collection(inner, def_idx, ctx)),
		CollectionExpr::Binary { op, left, right } => {
			let left = eval_collection(left, def_idx, ctx);
			let right = eval_collection(right, def_idx, ctx);
			match op {
				CollectionOp::Intersect => intersect(&left, &right),
				CollectionOp::Union => union(&left, &right),
				CollectionOp::Difference => difference(&left, &right),
			}
		}
	}
}

fn eval_pair_collection(
	collection: &CollectionExpr,
	scope: PairCollectionScope<'_>,
	ctx: &EvalCtx<'_, '_>,
) -> Option<Vec<Value>> {
	match collection {
		CollectionExpr::Projection(projection) => {
			Some(collect_projection(projection, scope.def_idx, ctx))
		}
		CollectionExpr::PairProjection(projection) => {
			Some(collect_pair_projection(projection, scope.a, scope.b, ctx))
		}
		CollectionExpr::Unique(inner) => eval_pair_collection(inner, scope, ctx).map(unique),
		CollectionExpr::Binary { op, left, right } => {
			let left = eval_pair_collection(left, scope, ctx)?;
			let right = eval_pair_collection(right, scope, ctx)?;
			Some(match op {
				CollectionOp::Intersect => intersect(&left, &right),
				CollectionOp::Union => union(&left, &right),
				CollectionOp::Difference => difference(&left, &right),
			})
		}
	}
}

fn collect_projection(
	projection: &CollectionProjection,
	def_idx: usize,
	ctx: &EvalCtx<'_, '_>,
) -> Vec<Value> {
	let mut values = Vec::new();
	for item in domain_items(&projection.domain, def_idx, ctx) {
		values.extend(project_item_path(item, &projection.path, ctx));
	}
	values
}

fn collect_pair_projection(
	projection: &PairCollectionProjection,
	a: DomainItem<'_>,
	b: DomainItem<'_>,
	ctx: &EvalCtx<'_, '_>,
) -> Vec<Value> {
	let DomainItem::Def { idx: Some(idx), .. } = (match projection.side {
		PairSide::A => a,
		PairSide::B => b,
	}) else {
		return Vec::new();
	};
	let mut values = Vec::new();
	for nested in domain_items(&projection.domain, idx, ctx) {
		values.extend(project_item_path(nested, &projection.path, ctx));
	}
	values
}

fn project_item_path(item: DomainItem<'_>, path: &[String], ctx: &EvalCtx<'_, '_>) -> Vec<Value> {
	match item {
		DomainItem::Def { idx, def } => {
			if let Some((head, tail)) = path.split_first()
				&& let Some(domain) = nested_domain(head)
				&& let Some(idx) = idx
			{
				let mut values = Vec::new();
				for nested in domain_items(&domain, idx, ctx) {
					values.extend(project_item_path(nested, tail, ctx));
				}
				return values;
			}
			project_def_path(def, path, ctx).into_iter().collect()
		}
		DomainItem::Ref { record } => project_ref_path(record, path, ctx).into_iter().collect(),
		DomainItem::Segment { kind, name } => {
			project_segment_path(kind, name, path).into_iter().collect()
		}
	}
}

fn nested_domain(segment: &str) -> Option<Domain> {
	match segment {
		"out_refs" => Some(Domain::OutRefs),
		"in_refs" => Some(Domain::InRefs),
		_ => None,
	}
}

fn project_def_path(
	def: &code_moniker_core::core::code_graph::DefRecord,
	path: &[String],
	ctx: &EvalCtx<'_, '_>,
) -> Option<Value> {
	let lhs = match path {
		[] => Lhs::Moniker,
		[name] if name == "self" => Lhs::Moniker,
		[name] if name == "name" => Lhs::Name,
		[name] if name == "kind" => Lhs::Kind,
		[name] if name == "shape" => Lhs::Shape,
		[name] if name == "visibility" => Lhs::Visibility,
		[name] if name == "srcset" => Lhs::Srcset,
		[name] if name == "lines" => Lhs::Lines,
		[name] if name == "start_line" => Lhs::StartLine,
		[name] if name == "end_line" => Lhs::EndLine,
		[name] if name == "start_byte" => Lhs::StartByte,
		[name] if name == "end_byte" => Lhs::EndByte,
		[name] if name == "depth" => Lhs::Depth,
		[name] if name == "parent" => Lhs::ParentMoniker,
		[parent, child] if parent == "parent" && child == "name" => Lhs::ParentName,
		[parent, child] if parent == "parent" && child == "kind" => Lhs::ParentKind,
		[parent, child] if parent == "parent" && child == "shape" => Lhs::ParentShape,
		_ => return None,
	};
	resolve_def_lhs(lhs, def, ctx)
}

fn project_ref_path(
	record: &code_moniker_core::core::code_graph::RefRecord,
	path: &[String],
	ctx: &EvalCtx<'_, '_>,
) -> Option<Value> {
	let lhs = match path {
		[] => Lhs::TargetMoniker,
		[name] if name == "kind" => Lhs::Kind,
		[name] if name == "start_line" => Lhs::StartLine,
		[name] if name == "end_line" => Lhs::EndLine,
		[name] if name == "start_byte" => Lhs::StartByte,
		[name] if name == "end_byte" => Lhs::EndByte,
		[name] if name == "source" => Lhs::SourceMoniker,
		[name] if name == "target" => Lhs::TargetMoniker,
		[source, parent] if source == "source" && parent == "parent" => Lhs::SourceParentMoniker,
		[target, parent] if target == "target" && parent == "parent" => Lhs::TargetParentMoniker,
		[source, child] if source == "source" && child == "name" => Lhs::SourceName,
		[source, child] if source == "source" && child == "kind" => Lhs::SourceKind,
		[source, child] if source == "source" && child == "shape" => Lhs::SourceShape,
		[source, child] if source == "source" && child == "visibility" => Lhs::SourceVisibility,
		[source, child] if source == "source" && child == "srcset" => Lhs::SourceSrcset,
		[target, child] if target == "target" && child == "name" => Lhs::TargetName,
		[target, child] if target == "target" && child == "kind" => Lhs::TargetKind,
		[target, child] if target == "target" && child == "shape" => Lhs::TargetShape,
		[target, child] if target == "target" && child == "visibility" => Lhs::TargetVisibility,
		[target, child] if target == "target" && child == "srcset" => Lhs::TargetSrcset,
		_ => return None,
	};
	resolve_ref_lhs(lhs, record, ctx)
}

fn project_segment_path(kind: &[u8], name: &[u8], path: &[String]) -> Option<Value> {
	match path {
		[projection] if projection == "kind" => {
			Some(Value::Str(std::str::from_utf8(kind).ok()?.to_string()))
		}
		[projection] if projection == "name" => {
			Some(Value::Str(std::str::from_utf8(name).ok()?.to_string()))
		}
		_ => None,
	}
}

fn unique(values: Vec<Value>) -> Vec<Value> {
	let mut seen = HashSet::new();
	let mut out = Vec::new();
	for value in values {
		let key = ValueKey::from_value(value.clone());
		if seen.insert(key) {
			out.push(value);
		}
	}
	out
}

fn intersect(left: &[Value], right: &[Value]) -> Vec<Value> {
	combine_counts(left, right, |l, r| l.min(r))
}

fn union(left: &[Value], right: &[Value]) -> Vec<Value> {
	combine_counts(left, right, |l, r| l.max(r))
}

fn difference(left: &[Value], right: &[Value]) -> Vec<Value> {
	combine_counts(left, right, |l, r| l.saturating_sub(r))
}

fn is_subset(left: &[Value], right: &[Value]) -> bool {
	let left = value_counts(left.iter().cloned());
	let right = value_counts(right.iter().cloned());
	left.into_iter()
		.all(|(key, count)| count <= right.get(&key).copied().unwrap_or(0))
}

fn combine_counts(
	left: &[Value],
	right: &[Value],
	merge: impl Fn(usize, usize) -> usize,
) -> Vec<Value> {
	let left_counts = value_counts(left.iter().cloned());
	let right_counts = value_counts(right.iter().cloned());
	let mut keys: HashMap<ValueKey, (usize, usize)> = HashMap::new();
	for (key, count) in left_counts {
		keys.entry(key).or_default().0 = count;
	}
	for (key, count) in right_counts {
		keys.entry(key).or_default().1 = count;
	}
	let mut out = Vec::new();
	for (key, (left_count, right_count)) in keys {
		for _ in 0..merge(left_count, right_count) {
			out.push(key.clone().into_value());
		}
	}
	out
}