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}