zen-engine 2.0.1

Business rules engine
Documentation
mod prefer_match;
mod redundant_parentheses;
mod repeated_derivation;
mod table_hygiene;

use std::sync::Arc;

use zen_expression::intellisense::AstMetadata;
use zen_expression::parser::Node;

use crate::policy::blocks::Block;
use crate::policy::ir::ParsedPolicy;
use crate::workspace::db::Db;
use crate::workspace::types::{Diagnostic, ExpressionKind, Span};

pub(crate) use prefer_match::PreferMatch;
pub(crate) use redundant_parentheses::RedundantParentheses;
pub(crate) use repeated_derivation::RepeatedDerivation;
pub(crate) use table_hygiene::{NonDiscriminatingColumn, RedundantTableRow};

pub(crate) trait LintRule {
    fn check(&self, cx: &LintContext, out: &mut Vec<Diagnostic>);
}

pub(crate) struct LintContext<'a> {
    db: &'a Db,
    target: &'a Arc<str>,
    parsed: Arc<ParsedPolicy>,
}

impl LintContext<'_> {
    pub(crate) fn target(&self) -> &Arc<str> {
        self.target
    }

    pub(crate) fn rules(&self) -> impl Iterator<Item = &Block> {
        self.parsed.policy.rules()
    }

    pub(crate) fn unit_policies(&self) -> Vec<(Arc<str>, Arc<ParsedPolicy>)> {
        let unit = self.db.unit(self.target);
        let mut members: Vec<Arc<str>> = unit.members.iter().cloned().collect();
        members.sort();
        members
            .into_iter()
            .filter_map(|path| self.db.parsed(&path).map(|parsed| (path, parsed)))
            .collect()
    }

    pub(crate) fn with_ast<T>(
        &self,
        source: &str,
        kind: ExpressionKind,
        f: impl for<'arena> FnOnce(&'arena Node<'arena>, &AstMetadata) -> T,
    ) -> Option<T> {
        let intellisense = self.db.intellisense();
        let mut intellisense = intellisense.borrow_mut();
        intellisense.with_ast(source, matches!(kind, ExpressionKind::Unary), f)
    }
}

pub(crate) struct AstOps;

impl AstOps {
    pub(crate) fn unwrap_parens<'a>(node: &'a Node<'a>) -> &'a Node<'a> {
        match node {
            Node::Parenthesized(inner) => Self::unwrap_parens(inner),
            _ => node,
        }
    }

    pub(crate) fn dotted_path(node: &Node) -> Option<String> {
        match node {
            Node::Identifier(name) => Some((*name).to_string()),
            Node::Member { node, property } => {
                let base = Self::dotted_path(Self::unwrap_parens(node))?;
                match Self::unwrap_parens(property) {
                    Node::String(p) => Some(format!("{base}.{p}")),
                    _ => None,
                }
            }
            _ => None,
        }
    }

    pub(crate) fn span(metadata: &AstMetadata, node: &Node) -> Option<Span> {
        metadata
            .get(&(node as *const Node as usize))
            .map(|m| m.span)
    }

    pub(crate) fn fingerprint(source: &str, span: Span) -> String {
        Self::chars_at(source, span)
            .filter(|c| !c.is_whitespace())
            .collect()
    }

    pub(crate) fn display_snippet(source: &str, span: Span) -> String {
        let mut out = String::new();
        let mut pending_space = false;
        for c in Self::chars_at(source, span) {
            if c.is_whitespace() {
                pending_space = !out.is_empty();
            } else {
                if pending_space {
                    out.push(' ');
                    pending_space = false;
                }
                out.push(c);
            }
        }
        if out.chars().count() > 60 {
            let truncated: String = out.chars().take(59).collect();
            return format!("{truncated}");
        }
        out
    }

    fn chars_at(source: &str, span: Span) -> impl Iterator<Item = char> + '_ {
        source
            .chars()
            .skip(span.0 as usize)
            .take((span.1 as usize).saturating_sub(span.0 as usize))
    }
}

pub(crate) struct Linter {
    rules: Vec<Box<dyn LintRule>>,
}

impl Linter {
    pub(crate) fn standard() -> Self {
        Self {
            rules: vec![
                Box::new(RepeatedDerivation),
                Box::new(PreferMatch),
                Box::new(RedundantTableRow),
                Box::new(NonDiscriminatingColumn),
                Box::new(RedundantParentheses),
            ],
        }
    }

    pub(crate) fn run(&self, db: &Db, target: &Arc<str>) -> Vec<Diagnostic> {
        let Some(parsed) = db.parsed(target) else {
            return Vec::new();
        };
        let cx = LintContext { db, target, parsed };
        let mut out = Vec::new();
        for rule in &self.rules {
            rule.check(&cx, &mut out);
        }
        out
    }
}