Skip to main content

linguini_ir/
reference.rs

1use crate::model::{
2    IrExpression, IrFunctionBranch, IrFunctionBranchValue, IrModule, IrText, IrTextPart,
3};
4use std::collections::{BTreeMap, BTreeSet};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct IrReferenceError {
8    pub message: String,
9}
10
11pub fn ensure_no_unresolved_references(
12    schema: &IrModule,
13    locale: &IrModule,
14) -> Result<(), Vec<IrReferenceError>> {
15    let context = ReferenceContext::new(schema, locale);
16    let mut errors = Vec::new();
17    let variables: BTreeSet<_> = locale
18        .variables
19        .iter()
20        .map(|variable| variable.name.clone())
21        .collect();
22
23    for message in &locale.messages {
24        let Some(parameters) = context.message_parameters.get(&message.name) else {
25            errors.push(IrReferenceError {
26                message: format!("unresolved message `{}`", message.name),
27            });
28            continue;
29        };
30        if let Some(body) = &message.body {
31            let variables = variables
32                .iter()
33                .cloned()
34                .chain(parameters.iter().cloned())
35                .collect();
36            check_text(body, &variables, &context, &mut errors);
37        }
38    }
39
40    for variable in &locale.variables {
41        check_text(&variable.value, &variables, &context, &mut errors);
42    }
43
44    for function in &locale.functions {
45        let variables: BTreeSet<_> = variables
46            .iter()
47            .cloned()
48            .chain(
49                function
50                    .parameters
51                    .iter()
52                    .filter_map(|parameter| parameter.name.clone()),
53            )
54            .collect();
55        for branch in &function.branches {
56            check_function_branch(branch, &variables, &context, &mut errors);
57        }
58    }
59
60    if errors.is_empty() {
61        Ok(())
62    } else {
63        Err(errors)
64    }
65}
66
67fn check_function_branch(
68    branch: &IrFunctionBranch,
69    variables: &BTreeSet<String>,
70    context: &ReferenceContext,
71    errors: &mut Vec<IrReferenceError>,
72) {
73    match &branch.value {
74        IrFunctionBranchValue::Text(text) => check_text(text, variables, context, errors),
75        IrFunctionBranchValue::Dispatch(branches) => {
76            for branch in branches {
77                check_function_branch(branch, variables, context, errors);
78            }
79        }
80    }
81}
82
83struct ReferenceContext {
84    message_parameters: BTreeMap<String, BTreeSet<String>>,
85    functions: BTreeSet<String>,
86    forms: BTreeSet<String>,
87    variables: BTreeSet<String>,
88}
89
90impl ReferenceContext {
91    fn new(schema: &IrModule, locale: &IrModule) -> Self {
92        Self {
93            message_parameters: schema
94                .messages
95                .iter()
96                .map(|message| {
97                    (
98                        message.name.clone(),
99                        message
100                            .parameters
101                            .iter()
102                            .map(|parameter| parameter.name.clone())
103                            .collect(),
104                    )
105                })
106                .collect(),
107            functions: locale
108                .functions
109                .iter()
110                .map(|function| function.name.clone())
111                .collect(),
112            forms: locale.forms.iter().map(|form| form.name.clone()).collect(),
113            variables: locale
114                .variables
115                .iter()
116                .map(|variable| variable.name.clone())
117                .collect(),
118        }
119    }
120}
121
122fn check_text(
123    text: &IrText,
124    variables: &BTreeSet<String>,
125    context: &ReferenceContext,
126    errors: &mut Vec<IrReferenceError>,
127) {
128    for part in &text.parts {
129        if let IrTextPart::Placeholder(expression) = part {
130            check_expression(expression, variables, context, errors);
131        }
132    }
133}
134
135fn check_expression(
136    expression: &IrExpression,
137    variables: &BTreeSet<String>,
138    context: &ReferenceContext,
139    errors: &mut Vec<IrReferenceError>,
140) {
141    let Some(root) = expression.path.first() else {
142        errors.push(IrReferenceError {
143            message: "unresolved empty expression".to_owned(),
144        });
145        return;
146    };
147
148    let resolved = variables.contains(root)
149        || context.forms.contains(root)
150        || context.functions.contains(root)
151        || context.variables.contains(root)
152        || root == "plural";
153
154    if !resolved {
155        errors.push(IrReferenceError {
156            message: format!("unresolved reference `{}`", expression.path.join(".")),
157        });
158    }
159
160    for argument in &expression.arguments {
161        check_expression(argument, variables, context, errors);
162    }
163}