Skip to main content

linguini_analyzer/
expression.rs

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}