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 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 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 pub bounds: Vec<String>,
68 pub line: usize,
69}
70
71const 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 pub path: Vec<String>,
79 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 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>,
115 pub children: Vec<FieldDeclaration>,
116 pub behaviors: Vec<String>,
117 pub exposed: Vec<String>,
118 pub owner: Option<String>,
119 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 pub text: Option<(String, usize)>,
157}
158
159#[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 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 #[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 #[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 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 #[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 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 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
719struct 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}