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:
[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:
[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 = truereport-derive/src/lib.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:
[package]
name = "report-runtime"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
report-derive = { path = "../report-derive" }report-runtime/src/lib.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:
[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(含全部负例输入和预期原因):
继续阅读
- 排错
- 排错
- 排错