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 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 #[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 #[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 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 #[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 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 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
727struct 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}