zen-engine 1.0.0-beta.11

Business rules engine
Documentation
use std::cell::RefCell;
use std::sync::Arc;

use ahash::{HashMap, HashMapExt, HashSet};
use zen_expression::parser::Node;

use crate::policy::blocks::BlockKind;
use crate::workspace::types::{
    Diagnostic, DiagnosticCode, DiagnosticLocation, ExpressionKind, Span,
};

use super::{AstOps, LintContext, LintRule};

pub(crate) struct RepeatedDerivation;

type SiteKey = (Arc<str>, Arc<str>, Arc<str>);

struct Occurrence {
    site: SiteKey,
    span: Span,
}

struct Fragment {
    ops: usize,
    display: String,
    policies: HashSet<Arc<str>>,
    occurrences: Vec<Occurrence>,
}

impl RepeatedDerivation {
    const SIMPLE_THRESHOLD: usize = 3;
    const COMPLEX_THRESHOLD: usize = 2;
    const COMPLEX_OPS: usize = 3;
    const MIN_FINGERPRINT_LEN: usize = 6;

    fn is_op(node: &Node) -> bool {
        match node {
            Node::Binary { .. }
            | Node::Unary { .. }
            | Node::Conditional { .. }
            | Node::FunctionCall { .. }
            | Node::MethodCall { .. }
            | Node::Interval { .. }
            | Node::Slice { .. }
            | Node::TemplateString(_) => true,
            Node::Object(entries) => !entries.is_empty(),
            Node::Array(items) => !items.is_empty(),
            _ => false,
        }
    }

    fn op_count(node: &Node) -> usize {
        let count = RefCell::new(0usize);
        node.walk(|n| {
            if Self::is_op(n) {
                *count.borrow_mut() += 1;
            }
        });
        count.into_inner()
    }

    fn threshold(ops: usize) -> usize {
        if ops >= Self::COMPLEX_OPS {
            Self::COMPLEX_THRESHOLD
        } else {
            Self::SIMPLE_THRESHOLD
        }
    }

    fn collect(cx: &LintContext) -> HashMap<String, Fragment> {
        let mut fragments: HashMap<String, Fragment> = HashMap::new();

        for (policy_path, parsed) in cx.unit_policies() {
            for block in parsed.policy.rules() {
                if matches!(block.kind, BlockKind::DecisionTable(_)) {
                    continue;
                }
                for location in block.kind.expressions(&block.id) {
                    if !matches!(location.kind, ExpressionKind::Standard) {
                        continue;
                    }
                    let site: SiteKey = (
                        policy_path.clone(),
                        location.block_id.clone(),
                        location.expression_id.clone(),
                    );
                    let found = cx
                        .with_ast(&location.source, location.kind, |root, metadata| {
                            let collected = RefCell::new(Vec::new());
                            root.walk(|node| {
                                if !Self::is_op(node) {
                                    return;
                                }
                                let Some(span) = AstOps::span(metadata, node) else {
                                    return;
                                };
                                collected.borrow_mut().push((span, Self::op_count(node)));
                            });
                            collected.into_inner()
                        })
                        .unwrap_or_default();

                    for (span, ops) in found {
                        let key = AstOps::fingerprint(&location.source, span);
                        if key.chars().count() < Self::MIN_FINGERPRINT_LEN {
                            continue;
                        }
                        let fragment = fragments.entry(key).or_insert_with(|| Fragment {
                            ops,
                            display: AstOps::display_snippet(&location.source, span),
                            policies: HashSet::default(),
                            occurrences: Vec::new(),
                        });
                        fragment.policies.insert(policy_path.clone());
                        fragment.occurrences.push(Occurrence {
                            site: site.clone(),
                            span,
                        });
                    }
                }
            }
        }

        fragments
    }
}

impl LintRule for RepeatedDerivation {
    fn check(&self, cx: &LintContext, out: &mut Vec<Diagnostic>) {
        let fragments = Self::collect(cx);

        let mut ordered: Vec<(String, Fragment)> = fragments
            .into_iter()
            .filter(|(_, f)| f.occurrences.len() >= Self::threshold(f.ops))
            .collect();
        ordered.sort_by(|a, b| {
            b.1.ops
                .cmp(&a.1.ops)
                .then_with(|| b.0.len().cmp(&a.0.len()))
                .then_with(|| a.0.cmp(&b.0))
        });

        let mut covered: HashMap<SiteKey, Vec<Span>> = HashMap::new();
        for (_, fragment) in ordered {
            let live: Vec<&Occurrence> = fragment
                .occurrences
                .iter()
                .filter(|occ| {
                    covered.get(&occ.site).is_none_or(|spans| {
                        !spans
                            .iter()
                            .any(|outer| outer.0 <= occ.span.0 && occ.span.1 <= outer.1)
                    })
                })
                .collect();
            if live.len() < Self::threshold(fragment.ops) {
                continue;
            }

            let count = live.len();
            let message = if fragment.policies.len() > 1 {
                format!(
                    "'{}' is derived {count} times across {} policies — compute it once into a shared property and reuse that",
                    fragment.display,
                    fragment.policies.len()
                )
            } else {
                format!(
                    "'{}' is derived {count} times — compute it once into a dedicated property and reuse that",
                    fragment.display
                )
            };

            for occ in &live {
                if occ.site.0 != *cx.target() {
                    continue;
                }
                out.push(Diagnostic::hint(
                    DiagnosticCode::RepeatedDerivation,
                    DiagnosticLocation::expression(
                        occ.site.0.clone(),
                        occ.site.1.clone(),
                        occ.site.2.clone(),
                        Some(occ.span),
                    ),
                    message.clone(),
                ));
            }

            for occ in live {
                covered.entry(occ.site.clone()).or_default().push(occ.span);
            }
        }
    }
}