Skip to main content

rapx/verify/contract/
attr.rs

1//! Parsing utilities for `#[rapx::requires(...)]` outer attributes.
2//!
3//! This module converts a raw `#[rapx::requires(...)]` attribute string into a
4//! structured representation that the verification analysis can consume without
5//! depending on `syn` expression details in later stages.
6//!
7//! The currently supported shape is:
8//!
9//! ```text
10//! #[rapx::requires(property_call, kind = "...")]
11//! ```
12//!
13//! where `kind = "..."` applies to the property in the same attribute.
14
15use syn::{
16    Expr, ExprCall, ExprPath, Lit, Result as SynResult, Token,
17    parse::{Parse, ParseStream},
18};
19
20use regex::Regex;
21use std::sync::LazyLock;
22
23/// A parsed `requires` property in the form `tag(arg0, arg1, ...)`.
24#[derive(Debug, Clone)]
25pub struct ParsedProperty {
26    /// The property name extracted from the call target.
27    pub tag: String,
28    /// The positional arguments passed to the property call.
29    pub args: Vec<Expr>,
30    /// Optional `kind` metadata associated with this property.
31    pub kind: Option<String>,
32}
33
34impl Parse for ParsedProperty {
35    /// Parse a single property item from a `requires` attribute argument list.
36    ///
37    /// Supported forms:
38    /// - `nonzero(x)`
39    /// - `nonzero(x), kind = "ptr"`
40    fn parse(input: ParseStream<'_>) -> SynResult<Self> {
41        let expr: Expr = input.parse()?;
42        let mut property = parse_property_expr(expr)?;
43
44        if input.peek(Token![,]) {
45            let fork = input.fork();
46            let _: Token![,] = fork.parse()?;
47            if fork.peek(syn::Ident) && fork.peek2(Token![=]) {
48                let _: Token![,] = input.parse()?;
49                let ident: syn::Ident = input.parse()?;
50                let _: Token![=] = input.parse()?;
51                let value: Expr = input.parse()?;
52
53                if ident == "kind" {
54                    if let Expr::Lit(ref expr_lit) = value
55                        && let Lit::Str(ref kind) = expr_lit.lit
56                    {
57                        property.kind = Some(kind.value());
58                    } else {
59                        return Err(syn::Error::new_spanned(
60                            value,
61                            "RAPx requires attribute kind must be a string literal",
62                        ));
63                    }
64                } else {
65                    return Err(syn::Error::new(
66                        ident.span(),
67                        "unsupported named RAPx requires attribute argument",
68                    ));
69                }
70            }
71        }
72
73        Ok(property)
74    }
75}
76
77/// A thin wrapper that allows parsing exactly one outer attribute from a string.
78struct RequireOuterAttribute {
79    attr: syn::Attribute,
80}
81
82impl Parse for RequireOuterAttribute {
83    /// Parse exactly one outer attribute.
84    fn parse(input: ParseStream<'_>) -> SynResult<Self> {
85        Ok(Self {
86            attr: input
87                .call(syn::Attribute::parse_outer)?
88                .into_iter()
89                .next()
90                .ok_or_else(|| input.error("expected exactly one outer attribute"))?,
91        })
92    }
93}
94
95/// Parse a raw attribute string into a structured `requires` property.
96///
97/// Returns `Ok(None)` when the attribute does not match `rapx::<expected_name>`
98/// or when it is not a list attribute.
99pub fn parse_rapx_attr(
100    attr_str: &str,
101    expected_name: &str,
102) -> SynResult<Option<ParsedProperty>> {
103    let attr_str = strip_lifetime_ticks(attr_str);
104    // Parse the raw string into a single outer attribute node.
105    let attr = syn::parse_str::<RequireOuterAttribute>(&attr_str)?.attr;
106    if !is_expected_syn_rapx_attr(&attr, expected_name) {
107        return Ok(None);
108    }
109
110    // Only list-style attributes carry an argument list.
111    let syn::Meta::List(meta_list) = &attr.meta else {
112        return Ok(None);
113    };
114
115    let property = meta_list.parse_args::<ParsedProperty>()?;
116    Ok(Some(property))
117}
118
119/// Check whether an attribute path is exactly `rapx::<expected_name>`.
120fn is_expected_syn_rapx_attr(attr: &syn::Attribute, expected_name: &str) -> bool {
121    let mut segments = attr.path().segments.iter();
122    matches!(
123        (segments.next(), segments.next(), segments.next()),
124        (Some(first), Some(second), None)
125            if first.ident == "rapx" && second.ident == expected_name
126    )
127}
128
129/// Parse a property call expression into a [`ParsedProperty`].
130fn parse_property_expr(expr: Expr) -> SynResult<ParsedProperty> {
131    match expr {
132        Expr::Call(ExprCall { func, args, .. }) => {
133            // Use the final segment of the callee path as the property tag.
134            let tag = match *func {
135                Expr::Path(ExprPath { path, .. }) => path
136                    .segments
137                    .last()
138                    .map(|seg| seg.ident.to_string())
139                    .ok_or_else(|| syn::Error::new_spanned(path, "missing property name"))?,
140                other => {
141                    return Err(syn::Error::new_spanned(
142                        other,
143                        "unsupported RAPx property callee expression",
144                    ));
145                }
146            };
147
148            Ok(ParsedProperty {
149                tag,
150                args: args.into_iter().collect(),
151                kind: None,
152            })
153        }
154        other => Err(syn::Error::new_spanned(
155            other,
156            "unsupported RAPx property expression",
157        )),
158    }
159}
160
161/// Strips the leading `'` from Rust lifetime tokens so that `syn` can
162/// parse them as regular identifier expressions inside attribute arguments.
163/// For example, `'a` becomes `a`, `'static` becomes `static`.
164static LIFETIME_TICK_RE: LazyLock<Regex> =
165    LazyLock::new(|| Regex::new(r"'([a-zA-Z_][a-zA-Z0-9_]*)").unwrap());
166
167fn strip_lifetime_ticks(s: &str) -> String {
168    LIFETIME_TICK_RE.replace_all(s, "$1").to_string()
169}