Skip to main content

sofie_model/
code.rs

1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use syn::ext::IdentExt;
5use syn::punctuated::Punctuated;
6use syn::visit::{self, Visit};
7use syn::{
8    Attribute, Expr, ExprCall, ExprMethodCall, Fields, ImplItem, Item, ItemEnum, ItemFn, ItemImpl,
9    ItemStruct, Macro, ReturnType, Token,
10};
11
12use crate::FeatureCode;
13use crate::constraints::described;
14use crate::effects::Effects;
15use crate::entitlements::{EntitlementDeclaration, entitlement_of};
16use crate::handlers::HandlerScope;
17use crate::naming::{DEFAULT_BEHAVIORS, behavior_columns, stored_name};
18use crate::syntax::{
19    doc_line, last_name, line_of, macro_expressions, path_ends_with, segments, string_literal,
20    type_last_name,
21};
22use crate::telemetry::TelemetryCode;
23
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct TypeName {
26    /// Last segment; `()` for unit, `(…)` for a tuple, `&` for a reference, `_` for anything else.
27    pub name: String,
28    pub arguments: Vec<TypeName>,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct Function {
33    pub path: Vec<String>,
34    pub arguments: Vec<TypeName>,
35    pub output: Option<TypeName>,
36    pub renders: Vec<ScreenRender>,
37    pub doc: Option<String>,
38    pub effects: Effects,
39    pub file: PathBuf,
40    pub line: usize,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct TypeMethod {
45    pub owner: String,
46    pub name: String,
47    /// A call of a trait's method by name does not show which impl runs.
48    pub implements: Option<String>,
49    pub doc: Option<String>,
50    pub effects: Effects,
51    pub file: PathBuf,
52    pub line: usize,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct ScreenRender {
57    pub screen: Option<String>,
58    pub line: usize,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct FieldDeclaration {
63    pub name: String,
64    pub ty: TypeName,
65    pub constraints: Vec<String>,
66    /// Each with its bound: `max 120`, `required`.
67    pub bounds: Vec<String>,
68    pub line: usize,
69}
70
71/// Must match what `#[derive(Input)]` and `#[derive(Aggregate)]` read.
72const CONSTRAINT_ATTRIBUTES: [&str; 6] = ["min", "max", "length", "range", "required", "unique"];
73
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct EnumDeclaration {
76    pub name: String,
77    /// From the crate's root, through a root re-export where there is one.
78    pub path: Vec<String>,
79    /// `PartlyShipped` is stored as `PARTLY_SHIPPED`.
80    pub stored_names: Vec<String>,
81    pub variants: Vec<EnumVariant>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct EnumVariant {
86    pub name: String,
87    pub to: Vec<String>,
88    /// `#[tone(success)]`: how a badge shows it.
89    pub tone: Option<String>,
90}
91
92impl EnumDeclaration {
93    #[must_use]
94    pub fn has_transitions(&self) -> bool {
95        self.variants.iter().any(|variant| !variant.to.is_empty())
96    }
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct ReadModel {
101    pub name: String,
102    pub from: Option<String>,
103    pub fields: Vec<FieldDeclaration>,
104    pub file: PathBuf,
105    pub line: usize,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct Aggregate {
110    pub name: String,
111    pub path: Vec<String>,
112    pub mirror: Option<Vec<String>>,
113    /// Stored columns; `Children` fields live in their own tables and are in `children`.
114    pub fields: Vec<FieldDeclaration>,
115    pub children: Vec<FieldDeclaration>,
116    pub behaviors: Vec<String>,
117    pub exposed: Vec<String>,
118    pub owner: Option<String>,
119    /// `#[owner]` on the struct: its own owner, or owned through its parent.
120    pub owner_on_struct: bool,
121    pub doc: Option<String>,
122    pub file: PathBuf,
123    pub line: usize,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct InputDeclaration {
128    pub name: String,
129    pub path: Vec<String>,
130    pub from: Option<String>,
131    pub fields: Vec<FieldDeclaration>,
132    pub unconstrained: Vec<String>,
133    pub file: PathBuf,
134    pub line: usize,
135}
136
137impl InputDeclaration {
138    #[must_use]
139    pub fn inherited<'a>(&self, field: &str, code: &'a Code) -> Option<&'a FieldDeclaration> {
140        let from = self.from.as_deref()?;
141        code.aggregates
142            .iter()
143            .find(|aggregate| aggregate.name == from)?
144            .fields
145            .iter()
146            .find(|declared| declared.name == field)
147    }
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct RawSql {
152    pub module_path: Vec<String>,
153    pub file: PathBuf,
154    pub line: usize,
155    /// The statement when written as a literal, with the line it starts on.
156    pub text: Option<(String, usize)>,
157}
158
159/// A translation key Rust code names as a literal, which the source language must have.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct KeyLiteral {
162    pub key: String,
163    pub file: PathBuf,
164    pub line: usize,
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq)]
168pub struct Code {
169    pub functions: Vec<Function>,
170    pub methods: Vec<TypeMethod>,
171    pub read_models: Vec<ReadModel>,
172    pub aggregates: Vec<Aggregate>,
173    pub inputs: Vec<InputDeclaration>,
174    pub enums: Vec<EnumDeclaration>,
175    /// The `#[derive(Value)]` types, which unlike enums get no derived `Fixture`.
176    pub values: Vec<String>,
177    pub raw_sql: Vec<RawSql>,
178    pub translation_keys: Vec<KeyLiteral>,
179    pub features: FeatureCode,
180    pub entitlements: Vec<EntitlementDeclaration>,
181    pub telemetry: TelemetryCode,
182}
183
184impl Code {
185    #[must_use]
186    pub fn function(&self, path: &[String]) -> Option<&Function> {
187        self.functions.iter().find(|function| function.path == path)
188    }
189
190    #[must_use]
191    pub fn read_model(&self, name: &str) -> Option<&ReadModel> {
192        self.read_models.iter().find(|model| model.name == name)
193    }
194
195    /// The module's own `#[derive(Value)]` types among `fields`.
196    #[must_use]
197    pub fn values_in<'f>(&self, fields: &'f [FieldDeclaration]) -> Vec<&'f str> {
198        let mut values: Vec<&str> = fields
199            .iter()
200            .flat_map(|field| std::iter::once(&field.ty).chain(&field.ty.arguments))
201            .map(|ty| ty.name.as_str())
202            .filter(|ty| self.values.iter().any(|value| value == ty))
203            .collect();
204        values.sort_unstable();
205        values.dedup();
206        values
207    }
208
209    #[must_use]
210    pub fn values_without_fixture<'f>(&self, fields: &'f [FieldDeclaration]) -> Vec<&'f str> {
211        let mut missing = self.values_in(fields);
212        missing.retain(|ty| {
213            !self.methods.iter().any(|method| {
214                method.owner == *ty && method.implements.as_deref() == Some("Fixture")
215            })
216        });
217        missing
218    }
219
220    #[must_use]
221    pub fn input_of(&self, function: &Function) -> Option<&InputDeclaration> {
222        function.arguments.iter().find_map(|argument| {
223            self.inputs
224                .iter()
225                .find(|input| argument.arguments.is_empty() && input.name == argument.name)
226        })
227    }
228
229    /// The input a GET reads from its query string, as `Query<Q>` or `Listing<T, Q>`.
230    #[must_use]
231    pub fn query_of(&self, function: &Function) -> Option<&InputDeclaration> {
232        function.arguments.iter().find_map(|argument| {
233            let (("Query", [parameters]) | ("Listing", [_, parameters])) =
234                (argument.name.as_str(), argument.arguments.as_slice())
235            else {
236                return None;
237            };
238            self.inputs
239                .iter()
240                .find(|input| parameters.arguments.is_empty() && input.name == parameters.name)
241        })
242    }
243
244    /// A contract cannot name the module's own types: an `Id<X>` becomes its uuid, a module enum its stored name.
245    pub(crate) fn contract_type(&self, ty: &TypeName) -> Option<String> {
246        let name = ty.name.as_str();
247        match ty.arguments.as_slice() {
248            [inner] if name == "Option" => Some(format!("Option<{}>", self.contract_type(inner)?)),
249            [_] if name == "Id" => Some("sofie::db::Uuid".to_owned()),
250            [] if self.enums.iter().any(|declared| declared.name == name) => {
251                Some("String".to_owned())
252            }
253            [] if CONTRACT_VALUES.contains(&name) => Some(format!("sofie::values::{name}")),
254            [] if CONTRACT_SCALARS.contains(&name) => Some(name.to_owned()),
255            _ => None,
256        }
257    }
258
259    pub(crate) fn collect(&mut self, item: &Item, file: &Path, scope: &HandlerScope) {
260        let module_path = scope.module_path();
261        self.features.collect(item, file, module_path);
262        self.entitlements
263            .extend(entitlement_of(item, file, module_path));
264        self.telemetry.collect(item, file, module_path);
265        let mut calls = SqlAndKeyCalls {
266            module_path,
267            file,
268            code: self,
269        };
270        calls.visit_item(item);
271        match item {
272            Item::Fn(function) => self.functions.push(function_of(function, file, scope)),
273            Item::Impl(implementation) => {
274                self.methods.extend(methods_of(implementation, file, scope));
275            }
276            Item::Struct(structure) if derives(&structure.attrs, "ReadModel") => {
277                self.read_models.push(read_model_of(structure, file));
278            }
279            Item::Struct(structure) if derives(&structure.attrs, "Aggregate") => {
280                self.aggregates
281                    .push(aggregate_of(structure, file, module_path));
282            }
283            Item::Struct(structure) if derives(&structure.attrs, "Input") => {
284                self.inputs.push(input_of(structure, file, module_path));
285            }
286            Item::Enum(enumeration) if derives(&enumeration.attrs, "Enum") => {
287                self.enums.push(enum_of(enumeration, module_path));
288            }
289            Item::Struct(structure) if derives(&structure.attrs, "Value") => {
290                self.values.push(structure.ident.to_string());
291            }
292            _ => {}
293        }
294    }
295}
296
297const CONTRACT_VALUES: [&str; 19] = [
298    "Coordinate",
299    "Currency",
300    "Date",
301    "Email",
302    "Energy",
303    "Instant",
304    "Length",
305    "LocalDateTime",
306    "Locale",
307    "Money",
308    "Percentage",
309    "PersonName",
310    "PhoneNumber",
311    "Slug",
312    "TimeOfDay",
313    "TimeZone",
314    "Url",
315    "Weekday",
316    "Weight",
317];
318
319const CONTRACT_SCALARS: [&str; 7] = ["String", "bool", "i16", "i32", "i64", "u16", "u32"];
320
321impl Aggregate {
322    pub fn contract_fields(&self, code: &Code) -> Result<Vec<(String, String)>, &FieldDeclaration> {
323        self.fields
324            .iter()
325            .filter(|field| self.exposed.contains(&field.name))
326            .map(|field| match code.contract_type(&field.ty) {
327                Some(ty) => Ok((field.name.clone(), ty)),
328                None => Err(field),
329            })
330            .collect()
331    }
332
333    pub fn all_behaviors(&self) -> impl Iterator<Item = &str> {
334        DEFAULT_BEHAVIORS
335            .into_iter()
336            .chain(self.behaviors.iter().map(String::as_str))
337    }
338
339    #[must_use]
340    pub fn is_owned(&self) -> bool {
341        self.owner.is_some() || self.owner_on_struct
342    }
343
344    #[must_use]
345    pub fn soft_deletes(&self) -> bool {
346        self.behaviors
347            .iter()
348            .any(|behavior| behavior == "SoftDelete")
349    }
350
351    #[must_use]
352    pub fn columns(&self) -> Vec<String> {
353        let behaviors = self.all_behaviors().flat_map(behavior_columns);
354        self.fields
355            .iter()
356            .map(|field| field.name.clone())
357            .chain(behaviors.map(|column| column.name.to_owned()))
358            .collect()
359    }
360}
361
362impl TypeName {
363    /// `syn` cannot see through id aliases, so `AccountId` names `Account` by naming convention.
364    #[must_use]
365    pub fn record(&self) -> Option<&str> {
366        match (self.name.as_str(), self.arguments.as_slice()) {
367            ("Option", [inner]) => inner.record(),
368            ("Id", [target]) => Some(&target.name),
369            (name, []) => name
370                .strip_suffix("Id")
371                .filter(|stem| stem.starts_with(char::is_uppercase)),
372            _ => None,
373        }
374    }
375
376    #[must_use]
377    pub fn without_result(&self) -> &Self {
378        match (self.name.as_str(), self.arguments.first()) {
379            ("Result", Some(inner)) => inner,
380            _ => self,
381        }
382    }
383
384    pub(crate) fn of(ty: &syn::Type) -> Self {
385        let (name, arguments) = match ty {
386            syn::Type::Path(path) if path.qself.is_none() => return Self::of_path(&path.path),
387            syn::Type::Tuple(tuple) if tuple.elems.is_empty() => ("()", Vec::new()),
388            syn::Type::Tuple(tuple) => ("(…)", tuple.elems.iter().map(Self::of).collect()),
389            syn::Type::Reference(reference) => ("&", vec![Self::of(&reference.elem)]),
390            syn::Type::Paren(inner) => return Self::of(&inner.elem),
391            syn::Type::TraitObject(object) => ("dyn", trait_bounds(&object.bounds)),
392            _ => ("_", Vec::new()),
393        };
394        Self {
395            name: name.to_owned(),
396            arguments,
397        }
398    }
399
400    fn of_path(path: &syn::Path) -> Self {
401        let Some(last) = path.segments.last() else {
402            return Self {
403                name: "_".to_owned(),
404                arguments: Vec::new(),
405            };
406        };
407        let arguments = match &last.arguments {
408            syn::PathArguments::AngleBracketed(bracketed) => bracketed
409                .args
410                .iter()
411                .filter_map(|argument| match argument {
412                    syn::GenericArgument::Type(ty) => Some(Self::of(ty)),
413                    _ => None,
414                })
415                .collect(),
416            _ => Vec::new(),
417        };
418        Self {
419            name: last.ident.unraw().to_string(),
420            arguments,
421        }
422    }
423}
424
425impl fmt::Display for TypeName {
426    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
427        let arguments: Vec<String> = self.arguments.iter().map(ToString::to_string).collect();
428        match self.name.as_str() {
429            "(…)" => write!(formatter, "({})", arguments.join(", ")),
430            "&" => write!(formatter, "&{}", arguments.join("")),
431            "dyn" => write!(formatter, "dyn {}", arguments.join(" + ")),
432            name if arguments.is_empty() => formatter.write_str(name),
433            name => write!(formatter, "{name}<{}>", arguments.join(", ")),
434        }
435    }
436}
437
438fn trait_bounds(bounds: &Punctuated<syn::TypeParamBound, Token![+]>) -> Vec<TypeName> {
439    bounds
440        .iter()
441        .filter_map(|bound| match bound {
442            syn::TypeParamBound::Trait(bound) => Some(TypeName::of_path(&bound.path)),
443            _ => None,
444        })
445        .collect()
446}
447
448fn function_of(function: &ItemFn, file: &Path, scope: &HandlerScope) -> Function {
449    let mut path = scope.module_path().to_vec();
450    path.push(function.sig.ident.to_string());
451    let mut renders = ScreenRenders::default();
452    renders.visit_block(&function.block);
453    let arguments = function
454        .sig
455        .inputs
456        .iter()
457        .filter_map(|argument| match argument {
458            syn::FnArg::Typed(typed) => Some(TypeName::of(&typed.ty)),
459            syn::FnArg::Receiver(_) => None,
460        })
461        .collect();
462    Function {
463        path,
464        arguments,
465        output: match &function.sig.output {
466            ReturnType::Default => None,
467            ReturnType::Type(_, ty) => Some(TypeName::of(ty)),
468        },
469        renders: renders.found,
470        doc: doc_line(&function.attrs),
471        effects: Effects::of(&function.sig, &function.block, None, scope),
472        file: file.to_path_buf(),
473        line: line_of(&function.sig.ident),
474    }
475}
476
477fn methods_of(implementation: &ItemImpl, file: &Path, scope: &HandlerScope) -> Vec<TypeMethod> {
478    let Some(owner) = type_last_name(&implementation.self_ty) else {
479        return Vec::new();
480    };
481    let implements = implementation
482        .trait_
483        .as_ref()
484        .and_then(|(.., path, _)| last_name(path));
485    implementation
486        .items
487        .iter()
488        .filter_map(|item| match item {
489            ImplItem::Fn(method) => Some(TypeMethod {
490                owner: owner.clone(),
491                name: method.sig.ident.unraw().to_string(),
492                implements: implements.clone(),
493                doc: doc_line(&method.attrs),
494                effects: Effects::of(&method.sig, &method.block, Some(&owner), scope),
495                file: file.to_path_buf(),
496                line: line_of(&method.sig.ident),
497            }),
498            _ => None,
499        })
500        .collect()
501}
502
503fn enum_of(enumeration: &ItemEnum, module_path: &[String]) -> EnumDeclaration {
504    let variants: Vec<EnumVariant> = enumeration
505        .variants
506        .iter()
507        .map(|variant| EnumVariant {
508            name: variant.ident.unraw().to_string(),
509            tone: variant
510                .attrs
511                .iter()
512                .filter(|attribute| attribute.path().is_ident("tone"))
513                .find_map(|attribute| attribute.parse_args::<syn::Ident>().ok())
514                .map(|tone| tone.to_string()),
515            to: variant
516                .attrs
517                .iter()
518                .filter(|attribute| attribute.path().is_ident("to"))
519                .filter_map(|attribute| {
520                    attribute
521                        .parse_args_with(Punctuated::<syn::Ident, Token![,]>::parse_terminated)
522                        .ok()
523                })
524                .flatten()
525                .map(|target| target.unraw().to_string())
526                .collect(),
527        })
528        .collect();
529    let name = enumeration.ident.unraw().to_string();
530    EnumDeclaration {
531        path: [module_path, std::slice::from_ref(&name)].concat(),
532        name,
533        stored_names: variants
534            .iter()
535            .map(|variant| stored_name(&variant.name))
536            .collect(),
537        variants,
538    }
539}
540
541fn read_model_of(structure: &ItemStruct, file: &Path) -> ReadModel {
542    ReadModel {
543        name: structure.ident.unraw().to_string(),
544        from: from_argument(&structure.attrs, "read"),
545        fields: fields_of(&structure.fields, |_| true),
546        file: file.to_path_buf(),
547        line: line_of(&structure.ident),
548    }
549}
550
551fn from_argument(attributes: &[Attribute], attribute: &str) -> Option<String> {
552    let mut from = None;
553    for attribute in attributes
554        .iter()
555        .filter(|candidate| candidate.path().is_ident(attribute))
556    {
557        // A malformed attribute is the derive's to report; the model only reads.
558        let _ = attribute.parse_nested_meta(|meta| {
559            if meta.path.is_ident("from") {
560                let path: syn::Path = meta.value()?.parse()?;
561                from = last_name(&path);
562            }
563            Ok(())
564        });
565    }
566    from
567}
568
569fn input_of(structure: &ItemStruct, file: &Path, module_path: &[String]) -> InputDeclaration {
570    let name = structure.ident.unraw().to_string();
571    let mut path = module_path.to_vec();
572    path.push(name.clone());
573    InputDeclaration {
574        name,
575        path,
576        from: from_argument(&structure.attrs, "input"),
577        fields: fields_of(&structure.fields, |_| true),
578        unconstrained: fields_marked(&structure.fields, "unconstrained").collect(),
579        file: file.to_path_buf(),
580        line: line_of(&structure.ident),
581    }
582}
583
584fn aggregate_of(structure: &ItemStruct, file: &Path, module_path: &[String]) -> Aggregate {
585    let mut behaviors = Vec::new();
586    for attribute in structure
587        .attrs
588        .iter()
589        .filter(|attr| attr.path().is_ident("aggregate"))
590    {
591        // A malformed attribute is the derive's to report; the model only reads.
592        let _ = attribute.parse_nested_meta(|meta| {
593            if meta.path.is_ident("behaviors") {
594                let content;
595                syn::parenthesized!(content in meta.input);
596                let named = Punctuated::<syn::Meta, Token![,]>::parse_terminated(&content)?;
597                behaviors.extend(
598                    named
599                        .iter()
600                        .filter_map(|behavior| last_name(behavior.path())),
601                );
602            }
603            Ok(())
604        });
605    }
606    let name = structure.ident.unraw().to_string();
607    let mirror = structure
608        .attrs
609        .iter()
610        .find(|attribute| attribute.path().is_ident("mirror"))
611        .and_then(|attribute| attribute.parse_args::<syn::Path>().ok())
612        .map(|path| segments(&path));
613    let (children, fields) = fields_of(&structure.fields, |name| name != "meta")
614        .into_iter()
615        .partition(|field| field.ty.name == "Children");
616    Aggregate {
617        path: [module_path, std::slice::from_ref(&name)].concat(),
618        name,
619        mirror,
620        fields,
621        children,
622        behaviors,
623        exposed: fields_marked(&structure.fields, "expose").collect(),
624        owner: fields_marked(&structure.fields, "owner").next(),
625        owner_on_struct: structure
626            .attrs
627            .iter()
628            .any(|attribute| attribute.path().is_ident("owner")),
629        doc: doc_line(&structure.attrs),
630        file: file.to_path_buf(),
631        line: line_of(&structure.ident),
632    }
633}
634
635fn fields_marked<'fields>(
636    fields: &'fields Fields,
637    attribute: &'fields str,
638) -> impl Iterator<Item = String> + 'fields {
639    fields
640        .iter()
641        .filter(move |field| {
642            field
643                .attrs
644                .iter()
645                .any(|candidate| candidate.path().is_ident(attribute))
646        })
647        .filter_map(|field| Some(field.ident.as_ref()?.unraw().to_string()))
648}
649
650fn fields_of(fields: &Fields, keep: impl Fn(&str) -> bool) -> Vec<FieldDeclaration> {
651    fields
652        .iter()
653        .filter_map(|field| {
654            let ident = field.ident.as_ref()?;
655            let name = ident.unraw().to_string();
656            let constrained: Vec<(&Attribute, String)> = field
657                .attrs
658                .iter()
659                .filter_map(|attribute| {
660                    let name = attribute.path().get_ident()?.to_string();
661                    CONSTRAINT_ATTRIBUTES
662                        .contains(&name.as_str())
663                        .then_some((attribute, name))
664                })
665                .collect();
666            keep(&name).then(|| FieldDeclaration {
667                name,
668                ty: TypeName::of(&field.ty),
669                bounds: constrained
670                    .iter()
671                    .flat_map(|(attribute, name)| described(attribute, name))
672                    .collect(),
673                constraints: constrained.into_iter().map(|(_, name)| name).collect(),
674                line: line_of(ident),
675            })
676        })
677        .collect()
678}
679
680pub(crate) fn derives(attributes: &[Attribute], derive: &str) -> bool {
681    attributes
682        .iter()
683        .filter(|attribute| attribute.path().is_ident("derive"))
684        .any(|attribute| {
685            let mut found = false;
686            let parsed = attribute.parse_nested_meta(|meta| {
687                found |= path_ends_with(&meta.path, &[derive]);
688                Ok(())
689            });
690            parsed.is_ok() && found
691        })
692}
693
694#[derive(Default)]
695struct ScreenRenders {
696    found: Vec<ScreenRender>,
697}
698
699impl Visit<'_> for ScreenRenders {
700    fn visit_expr_call(&mut self, call: &ExprCall) {
701        if let Expr::Path(function) = call.func.as_ref()
702            && path_ends_with(&function.path, &["Screen", "render"])
703        {
704            self.found.push(ScreenRender {
705                screen: call.args.first().and_then(string_literal),
706                line: line_of(call),
707            });
708        }
709        visit::visit_expr_call(self, call);
710    }
711
712    fn visit_macro(&mut self, call: &Macro) {
713        for expression in macro_expressions(call) {
714            self.visit_expr(&expression);
715        }
716    }
717}
718
719/// `.sql(…)` calls, and `FieldError::new(field, "key", message)`, whose second argument is a key.
720struct SqlAndKeyCalls<'code> {
721    module_path: &'code [String],
722    file: &'code Path,
723    code: &'code mut Code,
724}
725
726impl Visit<'_> for SqlAndKeyCalls<'_> {
727    fn visit_expr_call(&mut self, call: &ExprCall) {
728        if let Expr::Path(function) = call.func.as_ref()
729            && path_ends_with(&function.path, &["FieldError", "new"])
730            && let Some(key) = call.args.iter().nth(1)
731            && let Some(text) = string_literal(key)
732        {
733            self.code.translation_keys.push(KeyLiteral {
734                key: text,
735                file: self.file.to_path_buf(),
736                line: line_of(key),
737            });
738        }
739        visit::visit_expr_call(self, call);
740    }
741
742    fn visit_expr_method_call(&mut self, call: &ExprMethodCall) {
743        if call.method == "sql" {
744            self.code.raw_sql.push(RawSql {
745                module_path: self.module_path.to_vec(),
746                file: self.file.to_path_buf(),
747                line: line_of(&call.method),
748                text: call
749                    .args
750                    .first()
751                    .and_then(|first| Some((string_literal(first)?, line_of(first)))),
752            });
753        }
754        visit::visit_expr_method_call(self, call);
755    }
756
757    fn visit_macro(&mut self, call: &Macro) {
758        for expression in macro_expressions(call) {
759            self.visit_expr(&expression);
760        }
761    }
762}