1use proc_macro2::{Span, TokenStream, TokenTree};
2use quote::{ToTokens as _, quote};
3use syn::parse::{Parse, ParseStream, discouraged::Speculative as _};
4use syn::punctuated::Punctuated;
5use syn::spanned::Spanned as _;
6use syn::{AngleBracketedGenericArguments, Error, Expr, Ident, Path, Result, Token, parenthesized};
7
8pub const DEFAULT_COMPLETION_MARKER: &str = "raCompletionMarker";
15
16#[derive(Clone, Debug)]
18pub struct ChainParseOptions {
19 completion_marker: String,
20 allow_completion_probe: CompletionProbeParsing,
21}
22
23impl Default for ChainParseOptions {
24 fn default() -> Self {
25 Self {
26 completion_marker: DEFAULT_COMPLETION_MARKER.to_owned(),
27 allow_completion_probe: CompletionProbeParsing::Enabled,
28 }
29 }
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum CompletionProbeParsing {
35 Enabled,
37 Disabled,
39}
40
41impl CompletionProbeParsing {
42 fn is_enabled(self) -> bool {
43 matches!(self, Self::Enabled)
44 }
45}
46
47impl ChainParseOptions {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn completion_marker(mut self, marker: impl Into<String>) -> Self {
58 self.completion_marker = marker.into();
59 self
60 }
61
62 pub fn allow_completion_probe(mut self, allow: CompletionProbeParsing) -> Self {
69 self.allow_completion_probe = allow;
70 self
71 }
72
73 fn completion_marker_ident(&self) -> Result<Ident> {
74 syn::parse_str(&self.completion_marker).map_err(|_| {
75 Error::new(
76 Span::call_site(),
77 "completion marker must be a valid Rust identifier",
78 )
79 })
80 }
81}
82
83#[derive(Clone, Debug)]
89pub struct AttributeChain {
90 root: Path,
91 calls: Vec<ChainCall>,
92 completion: ChainCompletion,
93 span: Span,
94}
95
96impl AttributeChain {
97 pub fn parse_with_options(input: ParseStream<'_>, options: &ChainParseOptions) -> Result<Self> {
105 parse_chain_with_options(input, options)
106 }
107
108 pub fn parse_tokens_with_options(
115 tokens: TokenStream,
116 options: &ChainParseOptions,
117 ) -> Result<Self> {
118 syn::parse::Parser::parse2(
119 |input: ParseStream<'_>| Self::parse_with_options(input, options),
120 tokens,
121 )
122 }
123
124 pub fn root(&self) -> &Path {
125 &self.root
126 }
127
128 pub fn root_path(&self) -> &Path {
129 &self.root
130 }
131
132 pub fn calls(&self) -> &[ChainCall] {
133 &self.calls
134 }
135
136 pub fn completion(&self) -> &ChainCompletion {
137 &self.completion
138 }
139
140 pub fn has_completion_probe(&self) -> bool {
141 matches!(self.completion, ChainCompletion::DotProbe { .. })
142 }
143
144 pub fn completion_marker(&self) -> Option<&Ident> {
145 match &self.completion {
146 ChainCompletion::None => None,
147 ChainCompletion::DotProbe { marker } => Some(marker),
148 }
149 }
150
151 pub fn span(&self) -> Span {
152 self.span
153 }
154}
155
156impl Parse for AttributeChain {
157 fn parse(input: ParseStream<'_>) -> Result<Self> {
158 Self::parse_with_options(input, &ChainParseOptions::default())
159 }
160}
161
162impl quote::ToTokens for AttributeChain {
163 fn to_tokens(&self, tokens: &mut TokenStream) {
164 self.root.to_tokens(tokens);
165 for call in &self.calls {
166 call.to_tokens(tokens);
167 }
168 if let ChainCompletion::DotProbe { marker } = &self.completion {
169 quote! { .#marker }.to_tokens(tokens);
170 }
171 }
172}
173
174#[derive(Clone, Debug)]
176pub struct ChainCall {
177 method: Ident,
178 turbofish: Option<AngleBracketedGenericArguments>,
179 args: Vec<Expr>,
180}
181
182impl ChainCall {
183 pub fn method(&self) -> &Ident {
184 &self.method
185 }
186
187 pub fn turbofish(&self) -> Option<&AngleBracketedGenericArguments> {
188 self.turbofish.as_ref()
189 }
190
191 pub fn args(&self) -> &[Expr] {
192 &self.args
193 }
194}
195
196impl quote::ToTokens for ChainCall {
197 fn to_tokens(&self, tokens: &mut TokenStream) {
198 let method = &self.method;
199 let turbofish = &self.turbofish;
200 let args = &self.args;
201 quote! { .#method #turbofish (#(#args),*) }.to_tokens(tokens);
202 }
203}
204
205#[derive(Clone, Debug)]
207pub enum ChainCompletion {
208 None,
210 DotProbe {
212 marker: Ident,
214 },
215}
216
217#[derive(Clone, Debug)]
219pub struct ChainEntry {
220 label: Option<Ident>,
221 chain: AttributeChain,
222}
223
224impl ChainEntry {
225 pub fn label(&self) -> Option<&Ident> {
226 self.label.as_ref()
227 }
228
229 pub fn chain(&self) -> &AttributeChain {
230 &self.chain
231 }
232
233 pub fn into_chain(self) -> AttributeChain {
234 self.chain
235 }
236}
237
238impl Parse for ChainEntry {
239 fn parse(input: ParseStream<'_>) -> Result<Self> {
240 let label = parse_optional_label(input)?;
241 let chain = input.parse()?;
242 Ok(Self { label, chain })
243 }
244}
245
246#[derive(Clone, Debug, Default)]
248pub struct ChainList {
249 entries: Vec<ChainEntry>,
250}
251
252impl ChainList {
253 pub fn entries(&self) -> &[ChainEntry] {
254 &self.entries
255 }
256
257 pub fn into_entries(self) -> Vec<ChainEntry> {
258 self.entries
259 }
260
261 pub fn is_empty(&self) -> bool {
262 self.entries.is_empty()
263 }
264}
265
266impl Parse for ChainList {
267 fn parse(input: ParseStream<'_>) -> Result<Self> {
268 if input.is_empty() {
269 return Ok(Self::default());
270 }
271
272 let entries = Punctuated::<ChainEntry, Token![,]>::parse_terminated(input)?
273 .into_iter()
274 .collect();
275 Ok(Self { entries })
276 }
277}
278
279#[derive(Clone, Debug)]
281pub struct NamedChainGroup {
282 name: Ident,
283 entries: Vec<ChainEntry>,
284}
285
286impl NamedChainGroup {
287 pub fn name(&self) -> &Ident {
288 &self.name
289 }
290
291 pub fn entries(&self) -> &[ChainEntry] {
292 &self.entries
293 }
294}
295
296impl Parse for NamedChainGroup {
297 fn parse(input: ParseStream<'_>) -> Result<Self> {
298 let name = input.parse::<Ident>()?;
299 let content;
300 parenthesized!(content in input);
301 let entries = content.parse::<ChainList>()?.into_entries();
302 Ok(Self { name, entries })
303 }
304}
305
306fn parse_chain_with_options(
307 input: ParseStream<'_>,
308 options: &ChainParseOptions,
309) -> Result<AttributeChain> {
310 let fork = input.fork();
311 let (expr, advanced_fork) = match fork.parse::<Expr>() {
312 Ok(expr) => {
313 if !fork.is_empty() && fork.peek(Token![.]) {
314 match parse_trailing_dot_probe_expr(&fork, expr.to_token_stream(), options)? {
315 Some(expr) => (expr, fork),
316 None => return Err(invalid_chain_syntax_error(input)),
317 }
318 } else {
319 (expr, fork)
320 }
321 },
322 Err(_) => {
323 let fallback = input.fork();
324 match parse_trailing_dot_probe_expr(&fallback, TokenStream::new(), options)? {
325 Some(expr) => (expr, fallback),
326 None => return Err(invalid_chain_syntax_error(input)),
327 }
328 },
329 };
330 input.advance_to(&advanced_fork);
331
332 let span = expr.span();
333 let Some((root, calls, completion)) = analyze_chain_expr(&expr, true, options)? else {
334 return Err(Error::new(
335 span,
336 "expected attribute path or dot-call chain",
337 ));
338 };
339 Ok(AttributeChain {
340 root,
341 calls,
342 completion,
343 span,
344 })
345}
346
347fn parse_trailing_dot_probe_expr(
348 input: ParseStream<'_>,
349 mut probe_expr: TokenStream,
350 options: &ChainParseOptions,
351) -> Result<Option<Expr>> {
352 if !options.allow_completion_probe.is_enabled() {
353 return Ok(None);
354 }
355
356 let mut saw_token = false;
357 let mut ends_with_dot = false;
358
359 while !input.is_empty() && !input.peek(Token![,]) {
360 let token: TokenTree = input.parse()?;
361 ends_with_dot = matches!(&token, TokenTree::Punct(punct) if punct.as_char() == '.');
362 probe_expr.extend(token.to_token_stream());
363 saw_token = true;
364 }
365
366 if !saw_token || !ends_with_dot {
367 return Ok(None);
368 }
369
370 probe_expr.extend(options.completion_marker_ident()?.to_token_stream());
371 Ok(syn::parse2::<Expr>(probe_expr).ok())
372}
373
374fn analyze_chain_expr(
375 expr: &Expr,
376 is_terminal: bool,
377 options: &ChainParseOptions,
378) -> Result<Option<(Path, Vec<ChainCall>, ChainCompletion)>> {
379 match expr {
380 Expr::Group(group) => analyze_chain_expr(&group.expr, is_terminal, options),
381 Expr::Paren(paren) => analyze_chain_expr(&paren.expr, is_terminal, options),
382 Expr::Field(field) => {
383 let Some((root, calls, _completion)) = analyze_chain_expr(&field.base, false, options)?
384 else {
385 return Ok(None);
386 };
387
388 let syn::Member::Named(marker) = &field.member else {
389 return Ok(None);
390 };
391
392 if !is_terminal || marker != options.completion_marker.as_str() {
393 return Ok(None);
394 }
395 if !options.allow_completion_probe.is_enabled() {
396 return Ok(None);
397 }
398
399 Ok(Some((
400 root,
401 calls,
402 ChainCompletion::DotProbe {
403 marker: marker.clone(),
404 },
405 )))
406 },
407 Expr::MethodCall(method_call) => {
408 let Some((root, mut calls, _completion)) =
409 analyze_chain_expr(&method_call.receiver, false, options)?
410 else {
411 return Ok(None);
412 };
413
414 calls.push(ChainCall {
415 method: method_call.method.clone(),
416 turbofish: method_call.turbofish.clone(),
417 args: method_call.args.iter().cloned().collect(),
418 });
419 Ok(Some((root, calls, ChainCompletion::None)))
420 },
421 Expr::Path(path) => Ok(Some((path.path.clone(), Vec::new(), ChainCompletion::None))),
422 _ => Ok(None),
423 }
424}
425
426fn parse_optional_label(input: ParseStream<'_>) -> Result<Option<Ident>> {
427 if !input.peek(Ident) {
428 return Ok(None);
429 }
430
431 let fork = input.fork();
432 let _: Ident = fork.parse()?;
433 if !fork.peek(Token![=]) {
434 return Ok(None);
435 }
436
437 let label = input.parse::<Ident>()?;
438 input.parse::<Token![=]>()?;
439 Ok(Some(label))
440}
441
442fn invalid_chain_syntax_error(input: ParseStream<'_>) -> Error {
443 Error::new(
444 input.span(),
445 "attribute chain syntax expects a path such as `Thing::<_>` or a dot-call chain such as `Thing::<_>.option(value)`",
446 )
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use syn::{parse_quote, parse_str};
453
454 fn compact(tokens: impl quote::ToTokens) -> String {
455 tokens
456 .to_token_stream()
457 .to_string()
458 .chars()
459 .filter(|ch| !ch.is_whitespace())
460 .collect()
461 }
462
463 #[test]
464 fn parses_path_root_dot_chain() {
465 let chain: AttributeChain = parse_str("validators::RangeValidation::<_>.min(0).max(100)")
466 .expect("chain should parse");
467
468 assert_eq!(compact(chain.root()), "validators::RangeValidation::<_>");
469 assert_eq!(chain.calls().len(), 2);
470 assert_eq!(chain.calls()[0].method().to_string(), "min");
471 assert_eq!(chain.calls()[0].args().len(), 1);
472 assert_eq!(chain.calls()[1].method().to_string(), "max");
473 assert_eq!(
474 compact(&chain),
475 "validators::RangeValidation::<_>.min(0).max(100)"
476 );
477 }
478
479 #[test]
480 fn parses_root_only_chain_and_accessors() {
481 let chain: AttributeChain =
482 parse_str("::validators::RangeValidation").expect("root-only chain should parse");
483
484 assert_eq!(compact(chain.root_path()), "::validators::RangeValidation");
485 assert!(matches!(chain.completion(), ChainCompletion::None));
486 assert!(chain.completion_marker().is_none());
487 assert!(!chain.has_completion_probe());
488 let _span = chain.span();
489 }
490
491 #[test]
492 fn parses_method_turbofish_and_entry_accessors() {
493 let entry: ChainEntry =
494 parse_str("field = Validator.map::<String>(value)").expect("labeled entry");
495
496 assert_eq!(entry.label().map(ToString::to_string), Some("field".into()));
497 let chain = entry.chain();
498 assert_eq!(chain.calls().len(), 1);
499 assert_eq!(chain.calls()[0].method().to_string(), "map");
500 assert!(chain.calls()[0].turbofish().is_some());
501
502 let chain = entry.into_chain();
503 assert_eq!(compact(&chain), "Validator.map::<String>(value)");
504 }
505
506 #[test]
507 fn rejects_associated_call_root_dot_chain() {
508 let result = parse_str::<AttributeChain>("StringFaker::builder().with_min_length(5)");
509
510 assert!(result.is_err());
511 }
512
513 #[test]
514 fn rejects_non_chain_expressions() {
515 assert!(parse_str::<AttributeChain>("1u8..=3u8").is_err());
516 assert!(parse_str::<AttributeChain>("\"legacy shorthand\"").is_err());
517 assert!(parse_str::<AttributeChain>("make_faker() + other").is_err());
518 assert!(parse_str::<AttributeChain>("left + right.").is_err());
519 assert!(parse_str::<AttributeChain>("RangeValidation::<_>..").is_err());
520 }
521
522 #[test]
523 fn rejects_empty_chain() {
524 let result = parse_str::<AttributeChain>("");
525
526 assert!(result.is_err());
527 }
528
529 #[test]
530 fn rejects_field_expressions_that_are_not_completion_markers() {
531 assert!(parse_str::<AttributeChain>("true.raCompletionMarker").is_err());
532 assert!(parse_str::<AttributeChain>("RangeValidation.0").is_err());
533 assert!(parse_str::<AttributeChain>("RangeValidation.field").is_err());
534 }
535
536 #[test]
537 fn analyzes_parenthesized_and_grouped_chain_expressions() {
538 let chain: AttributeChain =
539 parse_str("(RangeValidation::<_>.min(0))").expect("parenthesized chain");
540 assert_eq!(compact(&chain), "RangeValidation::<_>.min(0)");
541
542 let expr = Expr::Group(syn::ExprGroup {
543 attrs: Vec::new(),
544 group_token: Default::default(),
545 expr: Box::new(parse_quote!(RangeValidation::<_>)),
546 });
547 let (root, calls, completion) =
548 analyze_chain_expr(&expr, true, &ChainParseOptions::default())
549 .expect("group analysis should parse")
550 .expect("grouped path should be a chain");
551 assert_eq!(compact(root), "RangeValidation::<_>");
552 assert!(calls.is_empty());
553 assert!(matches!(completion, ChainCompletion::None));
554 }
555
556 #[test]
557 fn parses_trailing_dot_completion_probe() {
558 let chain: AttributeChain =
559 parse_str("RangeValidation::<_>.min(0).").expect("trailing dot should recover");
560
561 assert!(chain.has_completion_probe());
562 assert_eq!(
563 chain.completion_marker().map(ToString::to_string),
564 Some(DEFAULT_COMPLETION_MARKER.to_owned())
565 );
566 assert_eq!(chain.calls().len(), 1);
567 assert_eq!(
568 compact(&chain),
569 "RangeValidation::<_>.min(0).raCompletionMarker"
570 );
571 }
572
573 #[test]
574 fn parses_root_trailing_dot_completion_probe() {
575 let chain: AttributeChain =
576 parse_str("RangeValidation::<_>.").expect("root trailing dot should recover");
577
578 assert!(chain.has_completion_probe());
579 assert_eq!(chain.calls().len(), 0);
580 assert_eq!(compact(&chain), "RangeValidation::<_>.raCompletionMarker");
581 }
582
583 #[test]
584 fn parses_root_trailing_dot_completion_probe_before_comma() {
585 let list: ChainList =
586 parse_str("RangeValidation::<_>., Other").expect("trailing dot before comma");
587
588 assert_eq!(list.entries().len(), 2);
589 assert!(list.entries()[0].chain().has_completion_probe());
590 assert_eq!(
591 compact(list.entries()[0].chain()),
592 "RangeValidation::<_>.raCompletionMarker"
593 );
594 }
595
596 #[test]
597 fn parses_named_completion_probe_with_infer_placeholder() {
598 let chain: AttributeChain = parse_str("RangeValidation::<_>.min(0).raCompletionMarker")
599 .expect("named completion marker should parse");
600
601 assert!(chain.has_completion_probe());
602 assert_eq!(chain.calls().len(), 1);
603 assert_eq!(
604 compact(&chain),
605 "RangeValidation::<_>.min(0).raCompletionMarker"
606 );
607 }
608
609 #[test]
610 fn rejects_trailing_dot_completion_probe_when_disabled() {
611 let options =
612 ChainParseOptions::new().allow_completion_probe(CompletionProbeParsing::Disabled);
613 let result = AttributeChain::parse_tokens_with_options(
614 quote!(RangeValidation::<i32>.min(0).),
615 &options,
616 );
617
618 assert!(result.is_err());
619 }
620
621 #[test]
622 fn rejects_root_trailing_dot_completion_probe_when_disabled() {
623 let options =
624 ChainParseOptions::new().allow_completion_probe(CompletionProbeParsing::Disabled);
625 let result =
626 AttributeChain::parse_tokens_with_options(quote!(RangeValidation::<i32>.), &options);
627
628 assert!(result.is_err());
629 }
630
631 #[test]
632 fn rejects_named_completion_probe_when_disabled() {
633 let options =
634 ChainParseOptions::new().allow_completion_probe(CompletionProbeParsing::Disabled);
635 let result = AttributeChain::parse_tokens_with_options(
636 quote!(RangeValidation::<i32>.min(0).raCompletionMarker),
637 &options,
638 );
639
640 assert!(result.is_err());
641 }
642
643 #[test]
644 fn parses_trailing_dot_completion_probe_with_custom_marker() {
645 let options = ChainParseOptions::new().completion_marker("completeHere");
646 let chain = AttributeChain::parse_tokens_with_options(
647 quote!(RangeValidation::<_>.min(0).),
648 &options,
649 )
650 .expect("trailing dot should recover with custom marker");
651
652 assert!(chain.has_completion_probe());
653 assert_eq!(
654 chain.completion_marker().map(ToString::to_string),
655 Some("completeHere".to_owned())
656 );
657 assert_eq!(compact(&chain), "RangeValidation::<_>.min(0).completeHere");
658 }
659
660 #[test]
661 fn rejects_invalid_custom_completion_marker() {
662 let options = ChainParseOptions::new().completion_marker("not a marker");
663 let err =
664 AttributeChain::parse_tokens_with_options(quote!(RangeValidation::<_>.), &options)
665 .expect_err("invalid completion marker should error");
666
667 assert!(
668 err.to_string()
669 .contains("completion marker must be a valid Rust identifier"),
670 "{err}"
671 );
672 }
673
674 #[test]
675 fn parses_empty_chain_list() {
676 let list: ChainList = parse_str("").expect("empty list");
677
678 assert!(list.is_empty());
679 assert!(list.entries().is_empty());
680 }
681
682 #[test]
683 fn parses_unlabeled_chain_entry_starting_with_colon() {
684 let entry: ChainEntry = parse_str("::Validator").expect("unlabeled absolute path");
685
686 assert!(entry.label().is_none());
687 assert_eq!(compact(entry.chain()), "::Validator");
688 }
689
690 #[test]
691 fn parses_labeled_chain_lists() {
692 let list: ChainList = syn::parse_quote! {
693 first = Validator::<_>.min(1),
694 Validator::<String>,
695 built = Faker.weight(2)
696 };
697
698 assert_eq!(list.entries().len(), 3);
699 assert_eq!(
700 list.entries()[0].label().map(ToString::to_string),
701 Some("first".to_owned())
702 );
703 assert!(list.entries()[1].label().is_none());
704 assert_eq!(
705 list.entries()[2].label().map(ToString::to_string),
706 Some("built".to_owned())
707 );
708 }
709
710 #[test]
711 fn parses_named_chain_group() {
712 let group: NamedChainGroup = syn::parse_quote! {
713 each(tag = TagFaker.length(8), OtherFaker)
714 };
715
716 assert_eq!(group.name().to_string(), "each");
717 assert_eq!(group.entries().len(), 2);
718 assert_eq!(
719 group.entries()[0].label().map(ToString::to_string),
720 Some("tag".to_owned())
721 );
722 }
723}