Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Introduction

attribute-dsl parses Rust proc-macro attributes made from a path root, dot-call chains, comma-separated entries, and named groups. It preserves syn syntax nodes so a macro can inspect the input, report spanned errors, and quote the parsed pieces into generated Rust.

This guide is for authors of derive and attribute macros. It assumes familiarity with syn, quote, and Rust token streams. The crate supports Rust 1.96 and edition 2024.

Use the crate when an attribute accepts syntax such as:

RootType::<_>.first(1).second::<String>("value")

The parsed model separates that input into a syn::Path root, ordered ChainCall values, and an optional completion-probe marker. Additional parsers cover labeled entries, comma-separated lists, and named parenthesized groups.

The infer helpers replace _ placeholders with an application-owned subject type without converting syntax trees to strings. This is useful when an attribute describes a builder or validator whose type depends on the annotated field.

Start with Getting started. Continue to Parse chain syntax when choosing a parser, Emit completion probes for rust-analyzer completion, and Substitute infer placeholders for subject-type substitution. Build a proc-macro expansion combines those operations into one derive-macro workflow.

Getting started

This path parses one path-rooted dot-call chain and inspects its root and calls. A successful cargo check confirms the parser and syn types resolve in the macro implementation crate.

Prerequisites

  • Rust 1.96 or newer.
  • A derive-macro or attribute-macro implementation crate.
  • A clear grammar for the attribute accepted by that macro.

Add dependencies

Add attribute-dsl alongside the syntax dependencies used by the macro:

[dependencies]
attribute-dsl = "0.1"
quote = "1.0"
syn = { features = [ "full" ], version = "2.0" }

Parse a chain

AttributeChain implements syn::parse::Parse, so it works with attribute argument parsing and syn::parse_str:

use attribute_dsl::AttributeChain;

let chain: AttributeChain =
    syn::parse_str("RootType::<_>.first(1).second::<String>(\"value\")")?;

assert_eq!(
    chain
        .root_path()
        .segments
        .last()
        .expect("a parsed path has a segment")
        .ident,
    "RootType"
);
assert_eq!(chain.calls().len(), 2);

Ok::<(), syn::Error>(())

In a proc macro, use attr.parse_args::<AttributeChain>()? to retain the attribute’s source spans in diagnostics.

Verify the integration

Run cargo check in the macro workspace. A successful check confirms that the crate versions resolve and the selected parser API is available.

If parsing fails, return the syn::Error from the macro expansion path or combine it with other spanned diagnostics. Do not replace it with a string-only error, because callers need the source span to locate invalid syntax.

Parse chain syntax

Choose the parser that matches the complete attribute arguments. Each parser retains paths, identifiers, and call arguments as syn nodes.

Input shapeParser
Root::<_>.first(1)AttributeChain
label = Root::<_>.first(1)ChainEntry
Root::<_>, other = Root::<i32>ChainList
fields(Root::<_>, other = Root::<i32>)NamedChainGroup

Supported grammar

An AttributeChain starts with a Rust path. Each following call requires a dot, a method name, and parentheses. Calls can include turbofish arguments and comma-separated syn::Expr arguments.

AttributeChain  := Path ("." Ident Turbofish? "(" Expr,* ")")* CompletionProbe?
CompletionProbe := "." CompletionMarker
ChainEntry      := (Ident "=")? AttributeChain
ChainList       := (ChainEntry ("," ChainEntry)* ","?)?
NamedChainGroup := Ident "(" ChainList ")"

Path includes module-qualified and absolute paths with normal Rust generic arguments. Parentheses around a complete chain are accepted and normalized to the same parsed model. A ChainList and the contents of a NamedChainGroup may be empty, and lists may end with a comma.

Inspect parsed values

Use the accessors instead of reparsing tokens:

use attribute_dsl::{AttributeChain, ChainList, NamedChainGroup};

let chain: AttributeChain = syn::parse_str("Root::<i32>.first(1)")?;
let first_call = &chain.calls()[0];
assert_eq!(first_call.method().to_string(), "first");
assert_eq!(first_call.args().len(), 1);

let list: ChainList =
    syn::parse_str("value = Root::<_>.first(1), Root::<String>")?;
assert_eq!(list.entries().len(), 2);
assert_eq!(
    list.entries()[0]
        .label()
        .expect("a labeled entry")
        .to_string(),
    "value"
);

let group: NamedChainGroup =
    syn::parse_str("fields(value = Root::<_>, Root::<String>)")?;
assert_eq!(group.name().to_string(), "fields");
assert_eq!(group.entries().len(), 2);

Ok::<(), syn::Error>(())

AttributeChain and ChainCall implement quote::ToTokens. An expansion that changes the root or completion behavior should quote the root, calls, and probe separately instead.

Keep constructors in the expansion

The chain root must remain a syn::Path; arbitrary Rust expressions are not accepted. This boundary keeps parsing predictable and leaves construction in the consumer macro.

Unsupported inputAction
Root::builder().first(1)Parse Root.first(1), then emit Root::builder() before the calls.
Root.fieldUse a method call, or reserve the configured terminal marker for completion probes.
left + rightParse the expression with syn directly instead of AttributeChain.

Perform domain checks after parsing and attach each syn::Error to the narrowest relevant path, method, or argument node.

Emit completion probes

Completion probes let an incomplete trailing dot produce rust-analyzer method completion inside an attribute token tree. Use them only when the macro emits the marker after a real typed receiver.

Handle a trailing dot

With the default options, Root::<_>.first(1). parses as a chain whose terminal marker is raCompletionMarker:

use attribute_dsl::AttributeChain;
use quote::quote;

let chain: AttributeChain = syn::parse_str("Root::<_>.first(1).")?;
assert!(chain.has_completion_probe());

let completion = chain
    .completion_marker()
    .map(|marker| quote! { .#marker })
    .unwrap_or_default();

Ok::<(), syn::Error>(())

Append completion after the application-owned constructor and parsed calls. At that position, the missing marker method gives rust-analyzer a typed receiver and allows it to offer the receiver’s methods at the original dot. The explicit terminal syntax Root::<_>.raCompletionMarker represents the same completion state.

Configure the marker

Use one stable Rust identifier when the consumer needs a custom marker:

use attribute_dsl::{AttributeChain, ChainParseOptions};
use quote::quote;

let options = ChainParseOptions::new().completion_marker("completeHere");
let chain = AttributeChain::parse_tokens_with_options(
    quote!(Root::<_>.first(1).),
    &options,
)?;

assert_eq!(
    chain
        .completion_marker()
        .expect("the input ends with a probe")
        .to_string(),
    "completeHere"
);

Ok::<(), syn::Error>(())

An invalid identifier produces a syn::Error when trailing-dot recovery needs to emit it. Custom ChainParseOptions apply to direct AttributeChain parsing; the Parse implementations for entries, lists, and groups use the defaults.

Reject probe syntax

Disable completion probes when the macro accepts only complete chains or cannot emit a typed receiver:

use attribute_dsl::{AttributeChain, ChainParseOptions, CompletionProbeParsing};
use quote::quote;

let options = ChainParseOptions::new()
    .allow_completion_probe(CompletionProbeParsing::Disabled);
let result = AttributeChain::parse_tokens_with_options(
    quote!(Root::<_>.first(1).),
    &options,
);

assert!(result.is_err());

Disabling probes rejects both trailing-dot recovery and an explicit terminal completion marker.

Preserve later list entries

Default completion recovery stops before a comma in ChainList, so later entries remain available while one entry is incomplete:

use attribute_dsl::ChainList;

let list: ChainList = syn::parse_str(
    "first = Root::<_>., second = Other::<String>",
)?;

assert!(list.entries()[0].chain().has_completion_probe());
assert_eq!(list.entries().len(), 2);

Ok::<(), syn::Error>(())

If parsing succeeds but rust-analyzer offers no methods, inspect the generated expression. The constructor and all preceding calls must resolve to the desired receiver type before the marker access.

Substitute infer placeholders

Use the infer helpers when _ in an attribute stands for a subject type known by the macro, such as an annotated field’s type. Each helper returns a new syntax tree and leaves the input node available to the caller.

HelperUse it for
split_terminal_single_type_argDistinguish an absent, inferred, or explicit final type argument.
substitute_infer_in_pathReplace _ inside path arguments.
substitute_infer_in_typeReplace _ in supported nested type forms.
substitute_infer_in_exprReplace _ in paths and types nested in an expression.

Inspect a terminal type argument

split_terminal_single_type_arg consumes a path, removes one terminal type argument, and returns SingleTypeArg::None, SingleTypeArg::Infer, or SingleTypeArg::Explicit:

use attribute_dsl::{SingleTypeArg, split_terminal_single_type_arg};
use syn::{Path, parse_quote};

let path: Path = parse_quote!(RootType::<_>);
let (root, argument) = split_terminal_single_type_arg(path, "validator")?;

assert_eq!(
    root.segments
        .last()
        .expect("a parsed path has a segment")
        .ident
        .to_string(),
    "RootType"
);
assert!(matches!(argument, SingleTypeArg::Infer));

Ok::<(), syn::Error>(())

The subject string appears in diagnostics. Use the consumer’s domain term, such as "validator" or "component", so errors identify the invalid path. The helper returns a syn::Error for multiple arguments, non-type arguments, or parenthesized arguments on the final segment.

Substitute nested placeholders

The path helper traverses generic arguments on every segment, including nested types, associated type values and constraints, and parenthesized function arguments and results.

The type helper supports:

  • _ and paths with type arguments;
  • arrays, slices, raw pointers, and references;
  • bare function inputs and results;
  • trait-object and impl Trait bounds;
  • tuples; and
  • parenthesized and grouped types.

The expression helper traverses the expression and applies the path and type rules wherever those nodes occur. A syn::Type variant outside the listed forms is cloned unchanged, so choose a consumer grammar whose placeholders are within the supported forms.

use attribute_dsl::{
    substitute_infer_in_expr, substitute_infer_in_path,
    substitute_infer_in_type,
};
use quote::ToTokens as _;
use syn::{Expr, Path, Type, parse_quote};

let replacement: Type = parse_quote!(i32);

let path: Path = parse_quote!(RootType::<Option<_>>);
let path = substitute_infer_in_path(&path, &replacement);
assert!(path.to_token_stream().to_string().contains("i32"));

let ty: Type = parse_quote!(fn([_; 2], &[_]) -> Option<_>);
let ty = substitute_infer_in_type(&ty, &replacement);
assert!(ty.to_token_stream().to_string().contains("i32"));

let expr: Expr = parse_quote!(RootType::<_>.first(Vec::<_>::new()));
let expr = substitute_infer_in_expr(&expr, &replacement);
assert!(expr.to_token_stream().to_string().contains("i32"));

Keep the replacement as a syn::Type and quote the returned tree directly. Converting through source strings discards syntax and span information needed for precise diagnostics.

Build a proc-macro expansion

Build an expansion by parsing each matching attribute, substituting its subject type, preserving every call in order, and appending a completion marker only when the parsed chain contains one.

Expand one attribute

For each attribute:

  1. Parse the arguments with attr.parse_args::<AttributeChain>()?.
  2. Substitute the field type into chain.root_path().
  3. Quote each call’s method, optional turbofish, and arguments.
  4. Quote chain.completion_marker() after the calls when it exists.
  5. Place the result after an application-owned constructor that returns the desired receiver type.
use attribute_dsl::{AttributeChain, substitute_infer_in_path};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Type};

fn expand_attribute(
    attr: &Attribute,
    field_ty: &Type,
    field_name: &str,
) -> syn::Result<TokenStream> {
    let chain = attr.parse_args::<AttributeChain>()?;
    let root = substitute_infer_in_path(chain.root_path(), field_ty);

    let calls = chain.calls().iter().map(|call| {
        let method = call.method();
        let turbofish = call.turbofish();
        let args = call.args();
        quote! { .#method #turbofish (#(#args),*) }
    });
    let completion = chain
        .completion_marker()
        .map(|marker| quote! { .#marker })
        .unwrap_or_default();

    Ok(quote! {
        #root::builder_for(#field_name) #(#calls)* #completion
    })
}

The consumer owns builder_for, its arguments, and the final generated item. Keeping construction outside the parser lets multiple macro crates share the chain grammar while producing different domain-specific code.

Preserve completion typing

A complete chain has no marker, so its normal expansion ends after the final call. For trailing-dot input, the marker must follow the same constructor and calls so rust-analyzer sees the real receiver type at the cursor. Disable probe parsing when the expansion cannot maintain that invariant.

Report and test failures

Return parsing and semantic failures as syn::Error values. Use syn::Error::new_spanned for consumer-owned checks so the diagnostic points to the relevant root, method, or argument.

Test at least:

  • a root-only chain;
  • ordered calls with arguments and turbofish syntax;
  • inferred and explicit subject types;
  • rejected non-path roots; and
  • trailing-dot input when completion probes are enabled.

The repository’s examples/derive_field_attrs.rs shows the same workflow in a complete executable derive-style example.