linguini-ir 0.1.0-alpha.4

Intermediate representation and lowering for Linguini
Documentation
use crate::model::{
    IrExpression, IrFunctionBranch, IrFunctionBranchValue, IrModule, IrText, IrTextPart,
};
use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IrReferenceError {
    pub message: String,
}

pub fn ensure_no_unresolved_references(
    schema: &IrModule,
    locale: &IrModule,
) -> Result<(), Vec<IrReferenceError>> {
    let context = ReferenceContext::new(schema, locale);
    let mut errors = Vec::new();
    let variables: BTreeSet<_> = locale
        .variables
        .iter()
        .map(|variable| variable.name.clone())
        .collect();

    for message in &locale.messages {
        let Some(parameters) = context.message_parameters.get(&message.name) else {
            errors.push(IrReferenceError {
                message: format!("unresolved message `{}`", message.name),
            });
            continue;
        };
        if let Some(body) = &message.body {
            let variables = variables
                .iter()
                .cloned()
                .chain(parameters.iter().cloned())
                .collect();
            check_text(body, &variables, &context, &mut errors);
        }
    }

    for variable in &locale.variables {
        check_text(&variable.value, &variables, &context, &mut errors);
    }

    for function in &locale.functions {
        let variables: BTreeSet<_> = variables
            .iter()
            .cloned()
            .chain(
                function
                    .parameters
                    .iter()
                    .filter_map(|parameter| parameter.name.clone()),
            )
            .collect();
        for branch in &function.branches {
            check_function_branch(branch, &variables, &context, &mut errors);
        }
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}

fn check_function_branch(
    branch: &IrFunctionBranch,
    variables: &BTreeSet<String>,
    context: &ReferenceContext,
    errors: &mut Vec<IrReferenceError>,
) {
    match &branch.value {
        IrFunctionBranchValue::Text(text) => check_text(text, variables, context, errors),
        IrFunctionBranchValue::Dispatch(branches) => {
            for branch in branches {
                check_function_branch(branch, variables, context, errors);
            }
        }
    }
}

struct ReferenceContext {
    message_parameters: BTreeMap<String, BTreeSet<String>>,
    functions: BTreeSet<String>,
    forms: BTreeSet<String>,
    variables: BTreeSet<String>,
}

impl ReferenceContext {
    fn new(schema: &IrModule, locale: &IrModule) -> Self {
        Self {
            message_parameters: schema
                .messages
                .iter()
                .map(|message| {
                    (
                        message.name.clone(),
                        message
                            .parameters
                            .iter()
                            .map(|parameter| parameter.name.clone())
                            .collect(),
                    )
                })
                .collect(),
            functions: locale
                .functions
                .iter()
                .map(|function| function.name.clone())
                .collect(),
            forms: locale.forms.iter().map(|form| form.name.clone()).collect(),
            variables: locale
                .variables
                .iter()
                .map(|variable| variable.name.clone())
                .collect(),
        }
    }
}

fn check_text(
    text: &IrText,
    variables: &BTreeSet<String>,
    context: &ReferenceContext,
    errors: &mut Vec<IrReferenceError>,
) {
    for part in &text.parts {
        if let IrTextPart::Placeholder(expression) = part {
            check_expression(expression, variables, context, errors);
        }
    }
}

fn check_expression(
    expression: &IrExpression,
    variables: &BTreeSet<String>,
    context: &ReferenceContext,
    errors: &mut Vec<IrReferenceError>,
) {
    let Some(root) = expression.path.first() else {
        errors.push(IrReferenceError {
            message: "unresolved empty expression".to_owned(),
        });
        return;
    };

    let resolved = variables.contains(root)
        || context.forms.contains(root)
        || context.functions.contains(root)
        || context.variables.contains(root)
        || root == "plural";

    if !resolved {
        errors.push(IrReferenceError {
            message: format!("unresolved reference `{}`", expression.path.join(".")),
        });
    }

    for argument in &expression.arguments {
        check_expression(argument, variables, context, errors);
    }
}