cyclonedx-bom-macros-0.1.0/.cargo_vcs_info.json0000644000000001620000000000100150560ustar { "git": { "sha1": "649dcba64d9e0e34e233450e0f1e7b96ea95d02d" }, "path_in_vcs": "cyclonedx-bom-macros" }cyclonedx-bom-macros-0.1.0/Cargo.toml0000644000000022510000000000100130550ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70.0" name = "cyclonedx-bom-macros" version = "0.1.0" authors = [ "Steve Springett ", "Amy Keibler <3483663+amy-keibler@users.noreply.github.com>", "Sergey \"Shnatsel\" Davidoff ", ] description = "Procedural macros used internally by the `cyclonedx-bom` crate" homepage = "https://cyclonedx.org/" license = "Apache-2.0" repository = "https://github.com/CycloneDX/cyclonedx-rust-cargo" [lib] proc-macro = true [dependencies.proc-macro2] version = "1.0.78" [dependencies.quote] version = "1.0.35" [dependencies.syn] version = "2.0.48" features = [ "full", "fold", ] [dev-dependencies.trybuild] version = "1.0" cyclonedx-bom-macros-0.1.0/Cargo.toml.orig000064400000000000000000000007301046102023000165360ustar 00000000000000[package] name = "cyclonedx-bom-macros" version = "0.1.0" description = "Procedural macros used internally by the `cyclonedx-bom` crate" authors.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true [lib] proc-macro = true [dev-dependencies] trybuild = "1.0" [dependencies] proc-macro2 = "1.0.78" quote = "1.0.35" syn = { version = "2.0.48", features = ["full", "fold"] } cyclonedx-bom-macros-0.1.0/src/lib.rs000064400000000000000000000260761046102023000155650ustar 00000000000000use std::error::Error as StdError; use std::str::FromStr; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::{ fold::{self, Fold}, parse_quote, punctuated::Punctuated, token::Comma, Error, Expr, Item, Stmt, }; #[derive(PartialEq, Eq)] struct Version { major: usize, minor: usize, } impl FromStr for Version { type Err = Box; fn from_str(s: &str) -> Result { let (major_str, minor_str) = s .split_once('.') .ok_or_else(|| Self::Err::from("missing `.`".to_owned()))?; Ok(Self { major: major_str.parse()?, minor: minor_str.parse()?, }) } } impl Version { fn as_ident(&self) -> syn::Ident { syn::Ident::new( &format!("v{}_{}", self.major, self.minor), Span::call_site(), ) } } enum VersionReq { Any(Vec), } impl syn::parse::Parse for VersionReq { fn parse(input: syn::parse::ParseStream) -> syn::Result { let versions = Punctuated::::parse_terminated(input)? .into_iter() .map(|s| s.value().parse().map_err(|err| Error::new(s.span(), err))) .collect::>>()?; Ok(Self::Any(versions)) } } impl VersionReq { fn matches(&self, version: &Version) -> bool { match self { VersionReq::Any(versions) => versions.contains(version), } } } struct VersionFilter { version: Version, error: Option, } impl VersionFilter { fn is_active(&mut self, attrs: &mut Vec) -> bool { let mut matches = true; attrs.retain(|attr| { let path = attr.path(); if path.is_ident("versioned") { match attr.parse_args::() { Ok(req) => matches = req.matches(&self.version), Err(err) => match self.error.as_mut() { Some(error) => error.combine(err), None => self.error = Some(err), }, } false } else { true } }); matches } fn filter_fields( &mut self, fields: Punctuated, ) -> Punctuated { fields .into_pairs() .filter_map(|mut pair| self.is_active(&mut pair.value_mut().attrs).then_some(pair)) .collect() } } impl Fold for VersionFilter { fn fold_fields_named(&mut self, mut fields: syn::FieldsNamed) -> syn::FieldsNamed { fields.named = self.filter_fields(fields.named); fields } fn fold_fields_unnamed(&mut self, mut fields: syn::FieldsUnnamed) -> syn::FieldsUnnamed { fields.unnamed = self.filter_fields(fields.unnamed); fields } fn fold_stmt(&mut self, mut stmt: Stmt) -> Stmt { if let Stmt::Local(syn::Local { ref mut attrs, .. }) | Stmt::Macro(syn::StmtMacro { ref mut attrs, .. }) = &mut stmt { if !self.is_active(attrs) { stmt = Stmt::Item(Item::Verbatim(TokenStream2::new())); } } fold::fold_stmt(self, stmt) } fn fold_expr(&mut self, mut expr: Expr) -> Expr { if let Expr::Array(syn::ExprArray { ref mut attrs, .. }) | Expr::Assign(syn::ExprAssign { ref mut attrs, .. }) | Expr::Async(syn::ExprAsync { ref mut attrs, .. }) | Expr::Await(syn::ExprAwait { ref mut attrs, .. }) | Expr::Binary(syn::ExprBinary { ref mut attrs, .. }) | Expr::Block(syn::ExprBlock { ref mut attrs, .. }) | Expr::Break(syn::ExprBreak { ref mut attrs, .. }) | Expr::Call(syn::ExprCall { ref mut attrs, .. }) | Expr::Cast(syn::ExprCast { ref mut attrs, .. }) | Expr::Closure(syn::ExprClosure { ref mut attrs, .. }) | Expr::Const(syn::ExprConst { ref mut attrs, .. }) | Expr::Continue(syn::ExprContinue { ref mut attrs, .. }) | Expr::Field(syn::ExprField { ref mut attrs, .. }) | Expr::ForLoop(syn::ExprForLoop { ref mut attrs, .. }) | Expr::Group(syn::ExprGroup { ref mut attrs, .. }) | Expr::If(syn::ExprIf { ref mut attrs, .. }) | Expr::Index(syn::ExprIndex { ref mut attrs, .. }) | Expr::Infer(syn::ExprInfer { ref mut attrs, .. }) | Expr::Let(syn::ExprLet { ref mut attrs, .. }) | Expr::Lit(syn::ExprLit { ref mut attrs, .. }) | Expr::Loop(syn::ExprLoop { ref mut attrs, .. }) | Expr::Macro(syn::ExprMacro { ref mut attrs, .. }) | Expr::Match(syn::ExprMatch { ref mut attrs, .. }) | Expr::MethodCall(syn::ExprMethodCall { ref mut attrs, .. }) | Expr::Paren(syn::ExprParen { ref mut attrs, .. }) | Expr::Path(syn::ExprPath { ref mut attrs, .. }) | Expr::Range(syn::ExprRange { ref mut attrs, .. }) | Expr::Reference(syn::ExprReference { ref mut attrs, .. }) | Expr::Repeat(syn::ExprRepeat { ref mut attrs, .. }) | Expr::Return(syn::ExprReturn { ref mut attrs, .. }) | Expr::Struct(syn::ExprStruct { ref mut attrs, .. }) | Expr::Try(syn::ExprTry { ref mut attrs, .. }) | Expr::TryBlock(syn::ExprTryBlock { ref mut attrs, .. }) | Expr::Tuple(syn::ExprTuple { ref mut attrs, .. }) | Expr::Unary(syn::ExprUnary { ref mut attrs, .. }) | Expr::Unsafe(syn::ExprUnsafe { ref mut attrs, .. }) | Expr::While(syn::ExprWhile { ref mut attrs, .. }) | Expr::Yield(syn::ExprYield { ref mut attrs, .. }) = &mut expr { if !self.is_active(attrs) { expr = parse_quote!({}); } } fold::fold_expr(self, expr) } fn fold_expr_struct(&mut self, mut expr: syn::ExprStruct) -> syn::ExprStruct { expr.fields = expr .fields .into_pairs() .filter_map(|mut pair| self.is_active(&mut pair.value_mut().attrs).then_some(pair)) .collect(); fold::fold_expr_struct(self, expr) } fn fold_expr_match(&mut self, mut expr: syn::ExprMatch) -> syn::ExprMatch { expr.arms.retain_mut(|arm| self.is_active(&mut arm.attrs)); fold::fold_expr_match(self, expr) } fn fold_item(&mut self, mut item: Item) -> Item { if let Item::Const(syn::ItemConst { ref mut attrs, .. }) | Item::Enum(syn::ItemEnum { ref mut attrs, .. }) | Item::ExternCrate(syn::ItemExternCrate { ref mut attrs, .. }) | Item::Fn(syn::ItemFn { ref mut attrs, .. }) | Item::ForeignMod(syn::ItemForeignMod { ref mut attrs, .. }) | Item::Impl(syn::ItemImpl { ref mut attrs, .. }) | Item::Macro(syn::ItemMacro { ref mut attrs, .. }) | Item::Mod(syn::ItemMod { ref mut attrs, .. }) | Item::Static(syn::ItemStatic { ref mut attrs, .. }) | Item::Struct(syn::ItemStruct { ref mut attrs, .. }) | Item::Trait(syn::ItemTrait { ref mut attrs, .. }) | Item::TraitAlias(syn::ItemTraitAlias { ref mut attrs, .. }) | Item::Type(syn::ItemType { ref mut attrs, .. }) | Item::Union(syn::ItemUnion { ref mut attrs, .. }) | Item::Use(syn::ItemUse { ref mut attrs, .. }) = &mut item { if !self.is_active(attrs) { item = Item::Verbatim(TokenStream2::new()); } } fold::fold_item(self, item) } fn fold_item_enum(&mut self, mut item: syn::ItemEnum) -> syn::ItemEnum { item.variants = item .variants .into_iter() .filter_map(|mut variant| self.is_active(&mut variant.attrs).then_some(variant)) .collect(); fold::fold_item_enum(self, item) } } fn helper(input: TokenStream, annotated_item: TokenStream) -> syn::Result { // This parses the module being annotated by the `#[versioned(..)]` attribute. let module = syn::parse::(annotated_item) .map_err(|err| Error::new(err.span(), format!("cannot parse module: {err}")))?; // This parses the versions passed to the attribute, e.g. the `"1.3"` // and `"1.4"`in `#[versioned("1.3", "1.4")] let versions = syn::parse::Parser::parse(Punctuated::::parse_terminated, input)? .into_iter() .map(|s| s.value().parse().map_err(|err| Error::new(s.span(), err))) .collect::>>()?; let content = module .content .as_ref() .ok_or_else(|| Error::new(module.ident.span(), "found module without content"))?; let mut tokens = TokenStream2::new(); for version in versions { let mod_vis = &module.vis; let mod_ident = version.as_ident(); let items = content.1.clone(); let mut folded_items = Vec::new(); let mut filter = VersionFilter { version, error: None, }; for item in items { folded_items.push(filter.fold_item(item)); if let Some(error) = filter.error { return Err(error); } } tokens.extend(quote! { #mod_vis mod #mod_ident { #(#folded_items)* } }) } Ok(tokens) } /// A `cfg`-like attribute macro to generate versioned modules. /// /// This macro allows to duplicate a module by providing version numbers to the /// macro itself. For example: /// ```rust /// use cyclonedx_bom_macros::versioned; /// /// #[versioned("1.0", "2.0")] /// mod base { /// pub(super) struct Foo; /// } /// ``` /// Will generate two modules: `v1_0` and `v2_0`, where each one of them /// contains the definition of `Foo`: /// ```rust /// mod v1_0 { /// pub(super) struct Foo; /// } /// /// mod v2_0 { /// pub(super) struct Foo; /// } /// ``` /// Additionally the macro can be used to gate definitions and expressions /// behind a specific version, very much like the `cfg` attribute. Based on the /// previous example: /// ```rust /// use cyclonedx_bom_macros::versioned; /// /// #[versioned("1.0", "2.0")] /// mod base { /// pub(super) struct Foo; /// /// #[versioned("2.0")] /// pub(super) struct Bar; /// } /// ``` /// The following code will be generated: /// ```rust /// mod v1_0 { /// pub(super) struct Foo; /// } /// /// mod v2_0 { /// pub(super) struct Foo; /// pub(super) struct Bar; /// } /// ``` /// Note that `Bar` only exists inside the `v2_0` module. Note that the /// `versioned` attribute annotating the module defines the versions that will be /// used to generate the modules and the attribute annotating the `Bar` definition /// states that this definition will only appear on the `2.0` module. /// /// Check the test folder for more usage examples. #[proc_macro_attribute] pub fn versioned(input: TokenStream, annotated_item: TokenStream) -> TokenStream { match helper(input, annotated_item) { Ok(tokens) => tokens, Err(err) => Error::new( err.span(), format!("{err} while using the `#[versioned]` macro"), ) .into_compile_error(), } .into() } cyclonedx-bom-macros-0.1.0/tests/lib.rs000064400000000000000000000002101046102023000161160ustar 00000000000000#[test] fn ui() { let t = trybuild::TestCases::new(); t.pass("tests/ui/pass/*.rs"); t.compile_fail("tests/ui/fail/*.rs"); } cyclonedx-bom-macros-0.1.0/tests/ui/fail/duplicated_struct.rs000064400000000000000000000003571046102023000224360ustar 00000000000000use cyclonedx_bom_macros::versioned; #[versioned("1.0", "2.0")] mod base { // Define `Foo` for 1.0 twice #[versioned("1.0")] pub struct Foo; #[versioned("1.0")] pub struct Foo; } fn main() { let _ = v1_0::Foo; } cyclonedx-bom-macros-0.1.0/tests/ui/fail/duplicated_struct.stderr000064400000000000000000000005361046102023000233140ustar 00000000000000error[E0428]: the name `Foo` is defined multiple times --> $DIR/duplicated_struct.rs:10:5 | 7 | pub struct Foo; | --------------- previous definition of the type `Foo` here ... 10 | pub struct Foo; | ^^^^^^^^^^^^^^^ `Foo` redefined here | = note: `Foo` must be defined only once in the type namespace of this module cyclonedx-bom-macros-0.1.0/tests/ui/fail/struct.rs000064400000000000000000000001241046102023000202300ustar 00000000000000use cyclonedx_bom_macros::versioned; #[versioned("1.3")] struct Foo; fn main() {} cyclonedx-bom-macros-0.1.0/tests/ui/fail/struct.stderr000064400000000000000000000002071046102023000211110ustar 00000000000000error: cannot parse module: expected `mod` while using the `#[versioned]` macro --> $DIR/struct.rs:4:1 | 4 | struct Foo; | ^^^^^^ cyclonedx-bom-macros-0.1.0/tests/ui/pass/import.rs000064400000000000000000000004451046102023000202570ustar 00000000000000use cyclonedx_bom_macros::versioned; #[versioned("1.0", "2.0")] mod base { // Define `PI` for 1.0 #[versioned("1.0")] pub const PI: f64 = 3.0; // Import `PI` for 2.0 #[versioned("2.0")] pub use std::f64::consts::PI; } fn main() { assert!(v1_0::PI < v2_0::PI); } cyclonedx-bom-macros-0.1.0/tests/ui/pass/multiple_versions_struct.rs000064400000000000000000000006571046102023000241410ustar 00000000000000use cyclonedx_bom_macros::versioned; #[versioned("1.0", "2.0", "3.0")] mod base { pub struct Foo { #[versioned("2.0", "3.0")] // This field only exists in versions 2.0 and 3.0. pub bar: u32, } } fn main() { // Version 1.0 does not have the `bar` field but 2.0 and 3.0 do. let _old_foo = v1_0::Foo {}; let _new_foo = v2_0::Foo { bar: 0 }; let _new_new_foo = v3_0::Foo { bar: 0 }; } cyclonedx-bom-macros-0.1.0/tests/ui/pass/struct_with_extra_field.rs000064400000000000000000000005451046102023000236730ustar 00000000000000use cyclonedx_bom_macros::versioned; #[versioned("1.0", "2.0")] mod base { pub struct Foo { #[versioned("2.0")] // This field only exists in version 2.0. pub bar: u32, } } fn main() { // Version 1.0 does not have the `bar` field but 2.0 does. let _old_foo = v1_0::Foo {}; let _new_foo = v2_0::Foo { bar: 0 }; }