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