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