thread-rule-engine 0.1.0

Rule-based scanning and transformation engine for Thread
Documentation
// SPDX-FileCopyrightText: 2022 Herrington Darkholme <2883231+HerringtonDarkholme@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT

use crate::RuleCore;
use crate::fixer::{Fixer, FixerError};
use crate::rule::Rule;
use crate::rule::referent_rule::RuleRegistration;
use crate::rule_config::RuleConfigError;
use crate::rule_core::RuleCoreError;
use crate::transform::{Transform, TransformError};

use thread_utilities::{RapidMap, RapidSet};

type RResult<T> = std::result::Result<T, RuleCoreError>;

pub enum CheckHint<'r> {
    Global,
    Normal,
    Rewriter(&'r RapidSet<&'r str>),
}

/// Different rule sections have different variable scopes/check procedure.
/// so we need to check rules with different hints.
pub fn check_rule_with_hint<'r>(
    rule: &'r Rule,
    utils: &'r RuleRegistration,
    constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
    transform: &'r Option<Transform>,
    fixer: &Vec<Fixer>,
    hint: CheckHint<'r>,
) -> RResult<()> {
    match hint {
        CheckHint::Global => {
            // do not check utils defined here because global rules are not yet ready
            check_vars(rule, utils, constraints, transform, fixer)?;
        }
        CheckHint::Normal => {
            check_utils_defined(rule, constraints)?;
            check_vars(rule, utils, constraints, transform, fixer)?;
        }
        // upper_vars is needed to check metavar defined in containing vars
        CheckHint::Rewriter(upper_vars) => {
            if fixer.is_empty() {
                return Err(RuleCoreError::Fixer(FixerError::InvalidRewriter));
            }
            check_utils_defined(rule, constraints)?;
            check_vars_in_rewriter(rule, utils, constraints, transform, fixer, upper_vars)?;
        }
    }
    Ok(())
}

fn check_vars_in_rewriter<'r>(
    rule: &'r Rule,
    utils: &'r RuleRegistration,
    constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
    transform: &'r Option<Transform>,
    fixer: &Vec<Fixer>,
    upper_var: &RapidSet<&str>,
) -> RResult<()> {
    let vars = get_vars_from_rules(rule, utils);
    let vars = check_var_in_constraints(vars, constraints)?;
    let mut vars = check_var_in_transform(vars, transform)?;
    for v in upper_var {
        vars.insert(v);
    }
    check_var_in_fix(vars, fixer)?;
    Ok(())
}

fn check_utils_defined(
    rule: &Rule,
    constraints: &RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
) -> RResult<()> {
    rule.verify_util()?;
    for constraint in constraints.values() {
        constraint.verify_util()?;
    }
    Ok(())
}

fn check_vars<'r>(
    rule: &'r Rule,
    utils: &'r RuleRegistration,
    constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
    transform: &'r Option<Transform>,
    fixer: &Vec<Fixer>,
) -> RResult<()> {
    let vars = get_vars_from_rules(rule, utils);
    let vars = check_var_in_constraints(vars, constraints)?;
    let vars = check_var_in_transform(vars, transform)?;
    check_var_in_fix(vars, fixer)?;
    Ok(())
}

fn get_vars_from_rules<'r>(rule: &'r Rule, utils: &'r RuleRegistration) -> RapidSet<&'r str> {
    let mut vars = rule.defined_vars();
    for var in utils.get_local_util_vars() {
        vars.insert(var);
    }
    vars
}

fn check_var_in_constraints<'r>(
    mut vars: RapidSet<&'r str>,
    constraints: &'r RapidMap<thread_ast_engine::meta_var::MetaVariableID, Rule>,
) -> RResult<RapidSet<&'r str>> {
    for rule in constraints.values() {
        for var in rule.defined_vars() {
            vars.insert(var);
        }
    }
    for var in constraints.keys() {
        let var: &str = var;
        if !vars.contains(var) {
            return Err(RuleCoreError::UndefinedMetaVar(
                var.to_owned(),
                "constraints",
            ));
        }
    }
    Ok(vars)
}

fn check_var_in_transform<'r>(
    mut vars: RapidSet<&'r str>,
    transform: &'r Option<Transform>,
) -> RResult<RapidSet<&'r str>> {
    let Some(transform) = transform else {
        return Ok(vars);
    };
    for var in transform.keys() {
        // vars already has the transform value. Report error!
        if !vars.insert(var) {
            return Err(RuleCoreError::Transform(TransformError::AlreadyDefined(
                var.to_string(),
            )));
        }
    }
    for trans in transform.values() {
        let needed = trans.used_vars();
        if !vars.contains(needed) {
            return Err(RuleCoreError::UndefinedMetaVar(
                needed.to_string(),
                "transform",
            ));
        }
    }
    Ok(vars)
}

fn check_var_in_fix(vars: RapidSet<&str>, fixers: &Vec<Fixer>) -> RResult<()> {
    for fixer in fixers {
        for var in fixer.used_vars() {
            if !vars.contains(&var) {
                return Err(RuleCoreError::UndefinedMetaVar(var.to_string(), "fix"));
            }
        }
    }
    Ok(())
}

pub fn check_rewriters_in_transform(
    rule: &RuleCore,
    rewriters: &RapidMap<String, RuleCore>,
) -> Result<(), RuleConfigError> {
    if let Some(err) = check_one_rewriter_in_rule(rule, rewriters) {
        return Err(err);
    }
    let error = rewriters
        .values()
        .find_map(|rewriter| check_one_rewriter_in_rule(rewriter, rewriters));
    if let Some(err) = error {
        return Err(err);
    }
    Ok(())
}

fn check_one_rewriter_in_rule(
    rule: &RuleCore,
    rewriters: &RapidMap<String, RuleCore>,
) -> Option<RuleConfigError> {
    let transform = rule.transform.as_ref()?;
    let mut used_rewriters = transform
        .values()
        .flat_map(|trans| trans.used_rewriters().iter());
    let undefined_writers = used_rewriters.find(|r| !rewriters.contains_key(*r))?;
    Some(RuleConfigError::UndefinedRewriter(
        undefined_writers.to_string(),
    ))
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::test::TypeScript;
    use crate::{DeserializeEnv, SerializableRuleCore, from_str};

    #[test]
    fn test_defined_vars() {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(
            r"
rule: {pattern: $A = $B}
constraints:
  A: { pattern: $C = $D }
transform:
  E:
    substring:
      source: $B
      startCar: 1",
        )
        .expect("should deser");
        let matcher = ser_rule.get_matcher(env).expect("should parse");
        assert_eq!(
            matcher.defined_vars(),
            ["A", "B", "C", "D", "E"].into_iter().collect()
        );
    }

    fn get_undefined(src: &str) -> (String, &str) {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(src).expect("should deser");
        match ser_rule.get_matcher(env) {
            Err(RuleCoreError::UndefinedMetaVar(name, section)) => (name, section),
            _ => panic!("unexpected error"),
        }
    }

    #[test]
    fn test_undefined_vars_in_constraints() {
        let (name, section) = get_undefined(
            r"
rule: {pattern: $A}
constraints: {B: {pattern: bbb}}
",
        );
        assert_eq!(name, "B");
        assert_eq!(section, "constraints");
    }
    #[test]
    fn test_undefined_vars_in_transform() {
        let (name, section) = get_undefined(
            r"
rule: {pattern: $A}
constraints: {A: {pattern: $C}}
transform:
  B:
    replace: {source: $C, replace: a, by: b }
  D:
    replace: {source: $E, replace: a, by: b }
",
        );
        assert_eq!(name, "E");
        assert_eq!(section, "transform");
    }
    #[test]
    fn test_undefined_vars_in_fix() {
        let (name, section) = get_undefined(
            r"
rule: {pattern: $A}
constraints: {A: {pattern: $C}}
transform:
  B:
    replace: {source: $C, replace: a, by: b }
fix: $D
",
        );
        assert_eq!(name, "D");
        assert_eq!(section, "fix");
    }

    #[test]
    fn test_defined_vars_in_utils() {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(
            r"
rule: {matches: test}
utils:
  test: { pattern: $B}",
        )
        .expect("should deser");
        let matcher = ser_rule.get_matcher(env).expect("should parse");
        assert_eq!(matcher.defined_vars(), ["B"].into_iter().collect());
    }

    #[test]
    fn test_use_vars_in_utils() {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(
            r"
utils:
  test: { pattern: $B }
rule: { matches: test }
fix: $B = 123",
        )
        .expect("should deser");
        let matcher = ser_rule.get_matcher(env).expect("should parse");
        assert_eq!(matcher.defined_vars(), ["B"].into_iter().collect());
    }

    #[test]
    fn test_defined_vars_cyclic() {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(
            r"
rule: { matches: test1 }
utils:
  test1: { pattern: $B, inside: {matches: test2} }
  test2: { pattern: $A, has: {matches: test1} }",
        )
        .expect("should deser");
        let matcher = ser_rule.get_matcher(env).expect("should parse");
        assert_eq!(matcher.defined_vars(), ["A", "B"].into_iter().collect());
    }

    #[test]
    fn test_transform_already_defined() {
        let env = DeserializeEnv::new(TypeScript::Tsx);
        let ser_rule: SerializableRuleCore = from_str(
            r"
rule: { pattern: $A = $B }
transform:
  B: { substring: { source: $A } }",
        )
        .expect("should deser");
        let matcher = ser_rule.get_matcher(env);
        match matcher {
            Err(RuleCoreError::Transform(TransformError::AlreadyDefined(b))) => {
                assert_eq!(b, "B");
            }
            _ => panic!("unexpected error"),
        }
    }
}