1use crate::{Diagnostic, QuickFix, Replacement};
2use linguini_syntax::{
3 Expression, FunctionBranchValue, FunctionDeclaration, LocaleDeclaration, LocaleFile, TextPart,
4 TextPattern,
5};
6use std::collections::BTreeMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Variable {
10 pub name: String,
11 pub ty: String,
12 pub span: linguini_syntax::Span,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct FormSignature {
17 pub type_name: String,
18 pub properties: Vec<FormProperty>,
19 pub span: linguini_syntax::Span,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FormProperty {
24 pub name: String,
25 pub span: linguini_syntax::Span,
26 pub needs_number: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct FunctionSignature {
31 pub name: String,
32 pub arity: usize,
33 pub span: linguini_syntax::Span,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct MessageToAnalyze {
38 pub name: String,
39 pub value: TextPattern,
40 pub variables: Vec<Variable>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ExpressionAnalysis {
45 pub variables: Vec<Variable>,
46 pub messages: Vec<MessageToAnalyze>,
47 pub functions: Vec<FunctionSignature>,
48 pub forms: Vec<FormSignature>,
49}
50
51impl Variable {
52 pub fn new(
53 name: impl Into<String>,
54 ty: impl Into<String>,
55 span: linguini_syntax::Span,
56 ) -> Self {
57 Self {
58 name: name.into(),
59 ty: ty.into(),
60 span,
61 }
62 }
63}
64
65impl FormProperty {
66 pub fn new(name: impl Into<String>, span: linguini_syntax::Span) -> Self {
67 Self {
68 name: name.into(),
69 span,
70 needs_number: false,
71 }
72 }
73
74 pub fn plural(name: impl Into<String>, span: linguini_syntax::Span) -> Self {
75 Self {
76 name: name.into(),
77 span,
78 needs_number: true,
79 }
80 }
81}
82
83impl FormSignature {
84 pub fn new(
85 type_name: impl Into<String>,
86 properties: Vec<FormProperty>,
87 span: linguini_syntax::Span,
88 ) -> Self {
89 Self {
90 type_name: type_name.into(),
91 properties,
92 span,
93 }
94 }
95}
96
97impl FunctionSignature {
98 pub fn new(name: impl Into<String>, arity: usize, span: linguini_syntax::Span) -> Self {
99 Self {
100 name: name.into(),
101 arity,
102 span,
103 }
104 }
105}
106
107impl MessageToAnalyze {
108 pub fn new(name: impl Into<String>, value: TextPattern, variables: Vec<Variable>) -> Self {
109 Self {
110 name: name.into(),
111 value,
112 variables,
113 }
114 }
115}
116
117pub fn analyze_expressions(input: ExpressionAnalysis) -> Vec<Diagnostic> {
118 let functions: BTreeMap<_, _> = input
119 .functions
120 .iter()
121 .map(|function| (function.name.as_str(), function))
122 .collect();
123 let forms: BTreeMap<_, _> = input
124 .forms
125 .iter()
126 .map(|form| (form.type_name.as_str(), form))
127 .collect();
128 let mut diagnostics = Vec::new();
129
130 for message in input.messages {
131 let all_variables = input
132 .variables
133 .iter()
134 .chain(message.variables.iter())
135 .collect::<Vec<_>>();
136 let variables: BTreeMap<_, _> = all_variables
137 .iter()
138 .map(|variable| (variable.name.as_str(), *variable))
139 .collect();
140 let numeric_variables = numeric_variables(&all_variables);
141 analyze_text(
142 &message.value,
143 &variables,
144 &functions,
145 &forms,
146 &numeric_variables,
147 &mut diagnostics,
148 );
149 }
150
151 diagnostics
152}
153
154pub fn analyze_function_patterns(file: &LocaleFile) -> Vec<Diagnostic> {
155 let mut diagnostics = Vec::new();
156 for declaration in &file.declarations {
157 collect_function_pattern_diagnostics(declaration, &mut diagnostics);
158 }
159 diagnostics
160}
161
162fn analyze_text(
163 text: &TextPattern,
164 variables: &BTreeMap<&str, &Variable>,
165 functions: &BTreeMap<&str, &FunctionSignature>,
166 forms: &BTreeMap<&str, &FormSignature>,
167 numeric_variables: &[&Variable],
168 diagnostics: &mut Vec<Diagnostic>,
169) {
170 for part in &text.parts {
171 if let TextPart::Placeholder(placeholder) = part {
172 analyze_expression(
173 &placeholder.expression,
174 variables,
175 functions,
176 forms,
177 numeric_variables,
178 diagnostics,
179 );
180 }
181 }
182}
183
184fn analyze_expression(
185 expression: &Expression,
186 variables: &BTreeMap<&str, &Variable>,
187 functions: &BTreeMap<&str, &FunctionSignature>,
188 forms: &BTreeMap<&str, &FormSignature>,
189 numeric_variables: &[&Variable],
190 diagnostics: &mut Vec<Diagnostic>,
191) {
192 for argument in &expression.arguments {
193 analyze_expression(
194 argument,
195 variables,
196 functions,
197 forms,
198 numeric_variables,
199 diagnostics,
200 );
201 }
202
203 if expression.path.is_empty() {
204 return;
205 }
206
207 if expression.arguments.is_empty() {
208 analyze_path(expression, variables, forms, numeric_variables, diagnostics);
209 } else {
210 analyze_call(
211 expression,
212 variables,
213 functions,
214 forms,
215 numeric_variables,
216 diagnostics,
217 );
218 }
219}
220
221fn analyze_path(
222 expression: &Expression,
223 variables: &BTreeMap<&str, &Variable>,
224 forms: &BTreeMap<&str, &FormSignature>,
225 numeric_variables: &[&Variable],
226 diagnostics: &mut Vec<Diagnostic>,
227) {
228 let root = &expression.path[0];
229 let Some(variable) = variables.get(root.value.as_str()) else {
230 diagnostics.push(Diagnostic::error(
231 format!("unknown variable `{}`", root.value),
232 root.span,
233 ));
234 return;
235 };
236
237 if expression.path.len() == 1 {
238 return;
239 }
240
241 let property = &expression.path[1];
242 let Some(form) = forms.get(variable.ty.as_str()) else {
243 diagnostics.push(Diagnostic::error(
244 format!("type `{}` has no form properties", variable.ty),
245 property.span,
246 ));
247 return;
248 };
249 let Some(property_signature) = form
250 .properties
251 .iter()
252 .find(|candidate| candidate.name == property.value)
253 else {
254 diagnostics.push(
255 Diagnostic::error(
256 format!(
257 "unknown form property `{}` on type `{}`",
258 property.value, variable.ty
259 ),
260 property.span,
261 )
262 .with_related(form.span, "form is declared here"),
263 );
264 return;
265 };
266
267 if property_signature.needs_number && numeric_variables.len() > 1 {
268 let expression_path = expression_path(expression);
269 let mut diagnostic = Diagnostic::error(
270 format!(
271 "ambiguous implicit plural argument for `{expression_path}`; pass a numeric argument explicitly",
272 ),
273 expression.span,
274 );
275 for variable in numeric_variables {
276 diagnostic = diagnostic.with_quick_fix(QuickFix::replacement(
277 format!("pass `{}` explicitly", variable.name),
278 Replacement {
279 span: expression.span,
280 text: format!("{expression_path}({})", variable.name),
281 },
282 ));
283 }
284 diagnostics.push(diagnostic);
285 }
286}
287
288fn analyze_call(
289 expression: &Expression,
290 variables: &BTreeMap<&str, &Variable>,
291 functions: &BTreeMap<&str, &FunctionSignature>,
292 forms: &BTreeMap<&str, &FormSignature>,
293 numeric_variables: &[&Variable],
294 diagnostics: &mut Vec<Diagnostic>,
295) {
296 if expression.path.len() == 1 {
297 let name = &expression.path[0];
298 if name.value == "plural" {
299 if expression.arguments.len() != 1 {
300 diagnostics.push(Diagnostic::error(
301 format!(
302 "function `plural` expects 1 argument(s), got {}",
303 expression.arguments.len()
304 ),
305 expression.span,
306 ));
307 }
308 return;
309 }
310
311 let Some(function) = functions.get(name.value.as_str()) else {
312 diagnostics.push(Diagnostic::error(
313 format!("unknown function `{}`", name.value),
314 name.span,
315 ));
316 return;
317 };
318
319 if function.arity != expression.arguments.len() {
320 diagnostics.push(
321 Diagnostic::error(
322 format!(
323 "function `{}` expects {} argument(s), got {}",
324 name.value,
325 function.arity,
326 expression.arguments.len()
327 ),
328 expression.span,
329 )
330 .with_related(function.span, "function is declared here"),
331 );
332 }
333 return;
334 }
335
336 analyze_path(expression, variables, forms, numeric_variables, diagnostics);
337}
338
339fn collect_function_pattern_diagnostics(
340 declaration: &LocaleDeclaration,
341 diagnostics: &mut Vec<Diagnostic>,
342) {
343 match declaration {
344 LocaleDeclaration::Function(function) => {
345 validate_function_branch_patterns(function, diagnostics);
346 }
347 LocaleDeclaration::Override(declaration) => {
348 collect_function_pattern_diagnostics(declaration, diagnostics);
349 }
350 _ => {}
351 }
352}
353
354fn validate_function_branch_patterns(
355 function: &FunctionDeclaration,
356 diagnostics: &mut Vec<Diagnostic>,
357) {
358 let dispatch_parameter_count = function
359 .parameters
360 .iter()
361 .filter(|parameter| parameter.ty.value != "String")
362 .count();
363 validate_branch_depth(
364 &function.branches,
365 function,
366 dispatch_parameter_count,
367 0,
368 diagnostics,
369 );
370}
371
372fn validate_branch_depth(
373 branches: &[linguini_syntax::FunctionBranch],
374 function: &FunctionDeclaration,
375 dispatch_parameter_count: usize,
376 depth: usize,
377 diagnostics: &mut Vec<Diagnostic>,
378) {
379 for branch in branches {
380 match &branch.value {
381 FunctionBranchValue::Text(_) if depth + 1 != dispatch_parameter_count => {
382 diagnostics.push(Diagnostic::error(
383 format!(
384 "function `{}` branch pattern expects {} value(s), got {}",
385 function.name.value,
386 dispatch_parameter_count,
387 depth + 1
388 ),
389 branch.span,
390 ));
391 }
392 FunctionBranchValue::Dispatch(branches) => {
393 validate_branch_depth(
394 branches,
395 function,
396 dispatch_parameter_count,
397 depth + 1,
398 diagnostics,
399 );
400 }
401 FunctionBranchValue::Text(_) => {}
402 }
403 }
404}
405
406fn numeric_variables<'a>(variables: &[&'a Variable]) -> Vec<&'a Variable> {
407 variables
408 .iter()
409 .filter(|variable| matches!(variable.ty.as_str(), "Number" | "Decimal"))
410 .copied()
411 .collect()
412}
413
414fn expression_path(expression: &Expression) -> String {
415 expression
416 .path
417 .iter()
418 .map(|name| name.value.as_str())
419 .collect::<Vec<_>>()
420 .join(".")
421}