Skip to content

Rust 过程宏:完整Report工作区与输入预期 · Report完整工作区至工程全部同源文件 ​

返回分篇目录

这是完整源码阅读工程,示例未执行,未执行cargo、rustc、测试、格式化或宏展开,也未安装依赖。目录在examples/rust/p2-proc-macros。代码中的断言与compile_fail都是未来验证规格,不是本批执行结果;页面通过include展示真实工程文件,避免另维护一份漂移副本。

Rust 过程宏:完整Report工作区与输入预期 ​

这是完整源码阅读工程,示例未执行,未执行cargo、rustc、测试、格式化或宏展开,也未安装依赖。目录在examples/rust/p2-proc-macros。代码中的断言与compile_fail都是未来验证规格,不是本批执行结果;页面通过include展示真实工程文件,避免另维护一份漂移副本。

Report完整工作区 ​

Report完整工作区含derive、runtime与consumer三个crate,Cargo使用workspace统一edition和依赖版本。runtime定义trait并提供derive再导出,consumer把依赖重命名为renamed_report,使用显式crate属性定位trait。这个拆分把语法转换和运行时接口放在各自合法的crate类型中,消费端同时担当集成边界。

工程使用edition 2021、std与固定直接依赖;不宣称支持所有Rust版本或no_std。没有Cargo.lock,因为本批没有解析依赖,不能编造锁文件或哈希。读者首次解析后应检查并保留锁文件,之后再谈离线与重复构建。

Report输入输出预期 ​

Report输入输出预期是AuditRow的name与value按声明顺序输出,value重命名为amount,secret被skip。sample的值应是标签name对应invoice、标签amount对应字符串7;借用报告后原记录的value和secret仍可访问。结果顺序由生成语句决定,不依赖HashSet迭代;HashSet只检查重复标签。

consumer的三个正例测试覆盖重命名依赖、生命周期/const泛型/原where条件、skip字段无需Display。十个compile_fail输入覆盖不支持项、tuple字段、重复与冲突属性、未知key、空/重名标签、Display缺失、缺少重命名路径、非法路径语法。期望原因写在源文件注释,未伪造编译器stderr。

工程全部同源文件 ​

下列清单包含全部清单文件、源文件、测试、输入负例、README与依赖说明。各文件均来自同一工作区;负例直接放在consumer库文档中,避免单独维护不受测试入口覆盖的文本fixture。未来运行步骤、失败归因、恢复与清理也在README内。

工作区Cargo.toml:

toml
[workspace]
members = ["report-runtime", "report-derive", "consumer"]
resolver = "2"

[workspace.package]
edition = "2021"
version = "0.1.0"
publish = false

[workspace.dependencies]
proc-macro2 = "=1.0.103"
quote = "=1.0.41"
syn = { version = "=2.0.108", features = ["full"] }

report-derive/Cargo.toml:

toml
[package]
name = "report-derive"
version.workspace = true
edition.workspace = true
publish.workspace = true

[lib]
proc-macro = true

[dependencies]
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true

report-derive/src/lib.rs:

rs
use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, LitStr, Path};

#[proc_macro_derive(Report, attributes(report))]
pub fn derive_report(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand(input).unwrap_or_else(syn::Error::into_compile_error).into()
}

fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let mut runtime: Option<Path> = None;
    for attr in &input.attrs {
        if !attr.path().is_ident("report") {
            continue;
        }
        attr.parse_nested_meta(|meta| {
            if !meta.path.is_ident("crate") {
                return Err(meta.error("item supports only report(crate = \"path\")"));
            }
            if runtime.is_some() {
                return Err(meta.error("duplicate report crate path"));
            }
            let value: LitStr = meta.value()?.parse()?;
            runtime = Some(value.parse()?);
            Ok(())
        })?;
    }
    let runtime = runtime.unwrap_or_else(|| syn::parse_quote!(::report_runtime));
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            other => return Err(syn::Error::new_spanned(other, "Report requires named fields")),
        },
        _ => return Err(syn::Error::new_spanned(&input.ident, "Report supports structs only")),
    };
    let mut generics = input.generics.clone();
    let mut rows = Vec::new();
    let mut labels = ::std::collections::HashSet::new();
    for field in fields {
        let mut skip = false;
        let mut rename: Option<LitStr> = None;
        for attr in &field.attrs {
            if !attr.path().is_ident("report") {
                continue;
            }
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("skip") {
                    if skip {
                        return Err(meta.error("duplicate report skip"));
                    }
                    skip = true;
                    Ok(())
                } else if meta.path.is_ident("rename") {
                    if rename.is_some() {
                        return Err(meta.error("duplicate report rename"));
                    }
                    rename = Some(meta.value()?.parse()?);
                    Ok(())
                } else {
                    Err(meta.error("field supports only report(skip) or report(rename = \"label\")"))
                }
            })?;
        }
        if skip && rename.is_some() {
            return Err(syn::Error::new_spanned(field, "report skip conflicts with rename"));
        }
        if skip {
            continue;
        }
        let ident = field.ident.as_ref().ok_or_else(|| {
            syn::Error::new_spanned(field, "Report requires named fields")
        })?;
        let label = rename.unwrap_or_else(|| {
            LitStr::new(ident.to_string().trim_start_matches("r#"), ident.span())
        });
        if label.value().is_empty() {
            return Err(syn::Error::new_spanned(&label, "report label must not be empty"));
        }
        if !labels.insert(label.value()) {
            return Err(syn::Error::new_spanned(&label, "duplicate report label"));
        }
        let ty = &field.ty;
        generics.make_where_clause().predicates.push(
            syn::parse_quote_spanned!(ty.span()=> #ty: ::std::fmt::Display)
        );
        rows.push(quote_spanned! {ty.span()=>
            rows.push((#label, ::std::format!("{}", &self.#ident)));
        });
    }
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
    Ok(quote! {
        impl #impl_generics #runtime::Report for #name #ty_generics #where_clause {
            fn report(&self) -> ::std::vec::Vec<(&'static str, ::std::string::String)> {
                let mut rows = ::std::vec::Vec::new();
                #(#rows)*
                rows
            }
        }
    })
}

report-runtime/Cargo.toml:

toml
[package]
name = "report-runtime"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
report-derive = { path = "../report-derive" }

report-runtime/src/lib.rs:

rs
//! Small runtime API and derive facade. No field values are inspected at compile time.
pub use report_derive::Report;

pub trait Report {
    fn report(&self) -> ::std::vec::Vec<(&'static str, ::std::string::String)>;
}

consumer/Cargo.toml:

toml
[package]
name = "report-consumer"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
renamed_report = { package = "report-runtime", path = "../report-runtime" }

consumer/src/lib.rs(含全部负例输入和预期原因):

继续阅读