Skip to main content

attribute_dsl/
chain.rs

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
8/// The method name rust-analyzer should complete against after a trailing dot.
9///
10/// Attribute parsers can accept incomplete syntax such as `TypeName.` by
11/// appending this marker, then generated code can emit a method access on the
12/// real builder value so rust-analyzer offers builder methods at the original
13/// cursor.
14pub const DEFAULT_COMPLETION_MARKER: &str = "raCompletionMarker";
15
16/// Parser options for [`AttributeChain`].
17#[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/// Whether completion-probe syntax is accepted while parsing a chain.
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum CompletionProbeParsing {
35    /// Accept trailing-dot recovery and explicit completion-marker syntax.
36    Enabled,
37    /// Reject trailing-dot recovery and explicit completion-marker syntax.
38    Disabled,
39}
40
41impl CompletionProbeParsing {
42    fn is_enabled(self) -> bool {
43        matches!(self, Self::Enabled)
44    }
45}
46
47impl ChainParseOptions {
48    /// Create parser options with completion probes enabled.
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Set the field/method marker inserted when the input ends in a dot.
54    ///
55    /// The marker must be a valid Rust identifier. Invalid markers cause
56    /// trailing-dot recovery to return a parse error.
57    pub fn completion_marker(mut self, marker: impl Into<String>) -> Self {
58        self.completion_marker = marker.into();
59        self
60    }
61
62    /// Set whether completion-probe parsing is enabled.
63    ///
64    /// Completion probes are useful for consumers that emit typed
65    /// rust-analyzer probe code. Consumers that only accept complete chains can
66    /// disable probes to reject both trailing-dot recovery and explicit marker
67    /// syntax.
68    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/// A parsed Rust attribute expression made from a path root and zero or more dot calls.
84///
85/// Accepted roots are:
86///
87/// - a path, such as `RangeValidation::<_>`.
88#[derive(Clone, Debug)]
89pub struct AttributeChain {
90    root: Path,
91    calls: Vec<ChainCall>,
92    completion: ChainCompletion,
93    span: Span,
94}
95
96impl AttributeChain {
97    /// Parse an [`AttributeChain`] from a [`ParseStream`] with custom options.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`syn::Error`] when the token stream is not a supported path or
102    /// dot-call chain, completion probes are disabled for probe syntax, or the
103    /// configured completion marker cannot be emitted as a Rust identifier.
104    pub fn parse_with_options(input: ParseStream<'_>, options: &ChainParseOptions) -> Result<Self> {
105        parse_chain_with_options(input, options)
106    }
107
108    /// Parse an [`AttributeChain`] from tokens with custom options.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`syn::Error`] for the same syntax and option failures as
113    /// [`Self::parse_with_options`].
114    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/// One dot-call in an [`AttributeChain`].
175#[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/// Completion state for a parsed chain.
206#[derive(Clone, Debug)]
207pub enum ChainCompletion {
208    /// The chain ended without a completion probe.
209    None,
210    /// The chain ended at a completion marker after a dot.
211    DotProbe {
212        /// Marker identifier emitted by completion-probe recovery.
213        marker: Ident,
214    },
215}
216
217/// A single chain entry, optionally labeled as `label = Chain`.
218#[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/// A comma-separated list of [`ChainEntry`] values.
247#[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/// A named parenthesized chain group such as `each(Thing.a(1), other = Thing.b(2))`.
280#[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}