diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a16cfb..120fbcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,60 +7,53 @@ on: branches: [ master, '[0-9]+.[0-9]+' ] jobs: - nightly: - name: Nightly, format and Doc - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Install toolchain - uses: dtolnay/rust-toolchain@nightly - - name: Build docs - run: cargo doc --all-features --all --no-deps - - name: Test impl-tools-lib - run: cargo test --manifest-path lib/Cargo.toml --all-features - - name: Test impl-tools - run: cargo test --all-features + test: + name: Test + strategy: + matrix: + include: + - os: ubuntu-latest + toolchain: nightly + variant: docs + - os: macos-latest + toolchain: beta + - os: windows-latest + toolchain: stable + - os: ubuntu-latest + toolchain: "1.70.0" + targets: "--lib --tests" + variant: MSRV - beta: - name: Beta on MacOS - runs-on: macos-latest + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 - - name: Install toolchain - uses: dtolnay/rust-toolchain@beta - - name: Test impl-tools-lib - run: cargo test --manifest-path lib/Cargo.toml --all-features - - name: Test impl-tools - run: cargo test --all-features + - name: Checkout + uses: actions/checkout@v5 - stable: - name: Stable on Windows - runs-on: windows-latest + - name: MSRV + if: ${{ matrix.variant == 'MSRV' }} + run: cp Cargo.lock.msrv Cargo.lock - steps: - - uses: actions/checkout@v5 - name: Install toolchain - uses: dtolnay/rust-toolchain@stable - - name: Test impl-tools-lib - run: cargo test --manifest-path lib/Cargo.toml --all-features - - name: Test impl-tools - run: cargo test --all-features + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} - msrv: - name: MSRV - runs-on: macos-latest + - name: Build docs + if: ${{ matrix.variant == 'docs' }} + run: cargo doc --all-features --all --no-deps - steps: - - uses: actions/checkout@v5 - - name: Install toolchain - uses: dtolnay/rust-toolchain@1.66.0 - - name: Use Cargo.lock.msrv - run: cp Cargo.lock.msrv Cargo.lock - name: Test impl-tools-lib - run: cargo test --manifest-path lib/Cargo.toml --all-features --lib --tests + run: cargo test --manifest-path lib/Cargo.toml --all-features ${{ matrix.targets }} + - name: Test impl-tools - run: cargo test --all-features --lib --tests + run: cargo test --all-features ${{ matrix.targets }} + + - name: Test test-cfg + working-directory: tests/test-cfg + run: | + cargo test + cargo test --features feature1 workspace: runs-on: ubuntu-latest @@ -69,7 +62,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: rustfmt + components: rustfmt,clippy - name: rustfmt run: cargo fmt --all -- --check - name: clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7c422..5c2f3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [0.12.0] — unreleased + +- Bump MSRV to 1.70 (#59) +- Let `#[autoimpl(Clone, Debug, Default)]` support `#[cfg]` on struct fields, enum variants and variant fields (#59) +- Let `#[autoimpl(Clone, Debug)]` support `ignore` on named enum fields (#59) + # [0.11.3] (lib only) - Fix `#[autoimpl(Clone)]` and `#[autoimpl(Hash)]` for non-`Copy` enums (#55, #56) diff --git a/Cargo.toml b/Cargo.toml index a654bc6..6975aab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [workspace] -members = ["lib"] +members = ["lib", "tests/test-cfg"] [workspace.package] authors = ["Diggory Hardy "] license = "MIT/Apache-2.0" repository = "https://github.com/kas-gui/impl-tools" -rust-version = "1.65.0" +rust-version = "1.70.0" edition = "2021" [package] diff --git a/README.md b/README.md index ed0af68..671d573 100644 --- a/README.md +++ b/README.md @@ -203,12 +203,6 @@ For an example of this approach, see [kas-macros](https://github.com/kas-gui/kas [`ScopeAttr`]: https://docs.rs/impl-tools-lib/latest/impl_tools_lib/trait.ScopeAttr.html -Supported Rust Versions ------------------------------- - -The MSRV is 1.65.0. - - Alternatives ------------ diff --git a/lib/src/autoimpl/impl_misc.rs b/lib/src/autoimpl/impl_misc.rs index bbe2f41..f7778fa 100644 --- a/lib/src/autoimpl/impl_misc.rs +++ b/lib/src/autoimpl/impl_misc.rs @@ -22,32 +22,58 @@ impl ImplTrait for ImplClone { true } - fn enum_items(&self, item: &ItemEnum, _: &ImplArgs) -> Result<(Toks, Toks)> { + fn enum_items(&self, item: &ItemEnum, args: &ImplArgs) -> Result<(Toks, Toks)> { let mut idfmt = IdentFormatter::new(); let name = &item.ident; let mut variants = Toks::new(); for v in item.variants.iter() { + for attr in &v.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut variants); + } + } + let ident = &v.ident; let tag = quote! { #name :: #ident }; variants.append_all(match v.fields { Fields::Named(ref fields) => { - let idents = fields.named.iter().map(|f| f.ident.as_ref().unwrap()); - let clones = fields.named.iter().map(|f| { - let ident = f.ident.as_ref().unwrap(); - quote! { #ident: ::core::clone::Clone::clone(#ident) } - }); - quote! { #tag { #(ref #idents),* } => #tag { #(#clones),* }, } + let mut idents = Toks::new(); + let mut toks = Toks::new(); + for field in fields.named.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut idents); + attr.to_tokens(&mut toks); + } + } + + let ident = field.ident.as_ref().unwrap(); + idents.append_all(quote! { ref #ident, }); + toks.append_all(if args.ignore_named(ident) { + quote! { #ident: Default::default(), } + } else { + quote! { #ident: ::core::clone::Clone::clone(#ident), } + }); + } + quote! { #tag { #idents } => #tag { #toks }, } } Fields::Unnamed(ref fields) => { - let len = fields.unnamed.len(); - let mut bindings = Vec::with_capacity(len); - let mut items = Vec::with_capacity(len); - for i in 0..len { + let mut bindings = Toks::new(); + let mut toks = Toks::new(); + for (i, field) in fields.unnamed.iter().enumerate() { let ident = idfmt.make_call_site(format_args!("_{i}")); - bindings.push(quote! { ref #ident }); - items.push(quote! { ::core::clone::Clone::clone(#ident) }); + + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut bindings); + attr.to_tokens(&mut toks); + } + } + + bindings.append_all(quote! { ref #ident, }); + toks.append_all(quote! { ::core::clone::Clone::clone(#ident), }); } - quote! { #tag ( #(#bindings),* ) => #tag ( #(#items),* ), } + quote! { #tag ( #bindings ) => #tag ( #toks ), } } Fields::Unit => quote! { #tag => #tag, }, }); @@ -68,26 +94,36 @@ impl ImplTrait for ImplClone { Fields::Named(fields) => { let mut toks = Toks::new(); for field in fields.named.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut toks); + } + } + let ident = field.ident.as_ref().unwrap(); - if args.ignore_named(ident) { - toks.append_all(quote! { #ident: Default::default(), }); + toks.append_all(if args.ignore_named(ident) { + quote! { #ident: Default::default(), } } else { - toks.append_all( - quote! { #ident: ::core::clone::Clone::clone(&self.#ident), }, - ); - } + quote! { #ident: ::core::clone::Clone::clone(&self.#ident), } + }); } quote! { #type_ident { #toks } } } Fields::Unnamed(fields) => { let mut toks = Toks::new(); - for i in 0..fields.unnamed.len() { + for (i, field) in fields.unnamed.iter().enumerate() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut toks); + } + } + let index = Index::from(i); - if args.ignore_unnamed(&index) { - toks.append_all(quote! { Default::default(), }); + toks.append_all(if args.ignore_unnamed(&index) { + quote! { Default::default(), } } else { - toks.append_all(quote! { ::core::clone::Clone::clone(&self.#index), }); - } + quote! { ::core::clone::Clone::clone(&self.#index), } + }); } quote! { #type_ident ( #toks ) } } @@ -133,7 +169,7 @@ impl ImplTrait for ImplDebug { true } - fn enum_items(&self, item: &ItemEnum, _: &ImplArgs) -> Result<(Toks, Toks)> { + fn enum_items(&self, item: &ItemEnum, args: &ImplArgs) -> Result<(Toks, Toks)> { let mut idfmt = IdentFormatter::new(); let name = &item.ident; let type_name = item.ident.to_string(); @@ -144,28 +180,44 @@ impl ImplTrait for ImplDebug { let tag = quote! { #name :: #ident }; variants.append_all(match v.fields { Fields::Named(ref fields) => { - let idents = fields.named.iter().map(|f| f.ident.as_ref().unwrap()); - let mut items = Toks::new(); + let mut idents = Toks::new(); + let mut stmts = quote! { let mut debug = f.debug_struct(#var_name); }; for field in fields.named.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut idents); + attr.to_tokens(&mut stmts); + } + } + let ident = field.ident.as_ref().unwrap(); - let name = ident.to_string(); - items.append_all(quote! { .field(#name, #ident) }); - } - quote! { - #tag { #(ref #idents),* } => f.debug_struct(#var_name) #items .finish(), + idents.append_all(quote! { ref #ident, }); + if !args.ignore_named(ident) { + let name = ident.to_string(); + stmts.append_all(quote! { { debug.field(#name, #ident); } }); + } } + stmts.append_all(quote! { debug.finish() }); + + quote! { #tag { #idents } => { #stmts }, } } Fields::Unnamed(ref fields) => { - let len = fields.unnamed.len(); - let mut bindings = Vec::with_capacity(len); - let mut items = Toks::new(); - for i in 0..len { + let mut bindings = Toks::new(); + let mut stmts = quote! { let mut debug = f.debug_tuple(#var_name); }; + for (i, field) in fields.unnamed.iter().enumerate() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut bindings); + attr.to_tokens(&mut stmts); + } + } + let ident = idfmt.make_call_site(format_args!("_{i}")); - bindings.push(quote! { ref #ident }); - items.append_all(quote! { .field(#ident) }); + bindings.append_all(quote! { ref #ident, }); + stmts.append_all(quote! { { debug.field(#ident); } }); } quote! { - #tag ( #(#bindings),* ) => f.debug_tuple(#var_name) #items .finish(), + #tag ( #bindings ) => { #stmts; debug.finish() }, } } Fields::Unit => quote! { #tag => f.write_str(#var_name), }, @@ -186,46 +238,52 @@ impl ImplTrait for ImplDebug { fn struct_items(&self, item: &ItemStruct, args: &ImplArgs) -> Result<(Toks, Toks)> { let type_name = item.ident.to_string(); - let mut inner; - match &item.fields { + let inner = match &item.fields { Fields::Named(fields) => { - inner = quote! { f.debug_struct(#type_name) }; + let mut stmts = quote! { let mut debug = f.debug_struct(#type_name); }; let mut no_skips = true; for field in fields.named.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut stmts); + } + } + let ident = field.ident.as_ref().unwrap(); if !args.ignore_named(ident) { let name = ident.to_string(); - inner.append_all(quote! { - .field(#name, &self.#ident) - }); + stmts.append_all(quote! { { debug.field(#name, &self.#ident); } }); } else { no_skips = false; } } if no_skips { - inner.append_all(quote! { .finish() }); + quote! { #stmts; debug.finish() } } else { - inner.append_all(quote! { .finish_non_exhaustive() }); - }; + quote! { #stmts; debug.finish_non_exhaustive() } + } } Fields::Unnamed(fields) => { - inner = quote! { f.debug_tuple(#type_name) }; - for i in 0..fields.unnamed.len() { + let mut stmts = quote! { let mut debug = f.debug_tuple(#type_name); }; + for (i, field) in fields.unnamed.iter().enumerate() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut stmts); + } + } + let index = Index::from(i); if !args.ignore_unnamed(&index) { - inner.append_all(quote! { - .field(&self.#index) - }); + stmts.append_all(quote! { { debug.field(&self.#index); } }); } else { - inner.append_all(quote! { - .field(&format_args!("_")) - }); + stmts.append_all(quote! { { debug.field(&format_args!("_")); } }); } } - inner.append_all(quote! { .finish() }); + quote! { #stmts; debug.finish() } } - Fields::Unit => inner = quote! { f.write_str(#type_name) }, + Fields::Unit => quote! { f.write_str(#type_name) }, }; + let method = quote! { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { #inner @@ -244,25 +302,37 @@ impl ImplTrait for ImplDefault { fn struct_items(&self, item: &ItemStruct, _: &ImplArgs) -> Result<(Toks, Toks)> { let type_ident = &item.ident; - let mut inner; - match &item.fields { + let inner = match &item.fields { Fields::Named(fields) => { - inner = quote! {}; + let mut toks = Toks::new(); for field in fields.named.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut toks); + } + } + let ident = field.ident.as_ref().unwrap(); - inner.append_all(quote! { #ident: Default::default(), }); + toks.append_all(quote! { #ident: Default::default(), }); } - inner = quote! { #type_ident { #inner } }; + quote! { #type_ident { #toks } } } Fields::Unnamed(fields) => { - inner = quote! {}; - for _ in 0..fields.unnamed.len() { - inner.append_all(quote! { Default::default(), }); + let mut toks = Toks::new(); + for field in fields.unnamed.iter() { + for attr in &field.attrs { + if attr.path().get_ident().is_some_and(|path| path == "cfg") { + attr.to_tokens(&mut toks); + } + } + + toks.append_all(quote! { Default::default(), }); } - inner = quote! { #type_ident(#inner) }; + quote! { #type_ident(#toks) } } - Fields::Unit => inner = quote! { #type_ident }, - } + Fields::Unit => quote! { #type_ident }, + }; + let method = quote! { fn default() -> Self { #inner diff --git a/tests/for_deref.rs b/tests/for_deref.rs index 90d5ed9..f2b9cfc 100644 --- a/tests/for_deref.rs +++ b/tests/for_deref.rs @@ -11,6 +11,7 @@ use impl_tools::autoimpl; #[autoimpl(for<'a, T: trait> &'a mut T, Box)] trait Z { + #[allow(unused)] const A: i32; fn f(&self); diff --git a/tests/test-cfg/Cargo.toml b/tests/test-cfg/Cargo.toml new file mode 100644 index 0000000..43e19d4 --- /dev/null +++ b/tests/test-cfg/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test-cfg" +version = "0.1.0" +authors.workspace = true +license.workspace = true +rust-version.workspace = true +edition.workspace = true +publish = false + +[features] +feature1 = [] + +[dependencies] +impl-tools = { version = "0.11", path = "../.." } diff --git a/tests/test-cfg/tests/autoimpl.rs b/tests/test-cfg/tests/autoimpl.rs new file mode 100644 index 0000000..1a8e065 --- /dev/null +++ b/tests/test-cfg/tests/autoimpl.rs @@ -0,0 +1,74 @@ +//! Test that autoimpl can handle cfg on fields + +#![allow(non_snake_case)] + +use impl_tools::autoimpl; + +#[autoimpl(Clone, Debug, Default where T: trait)] +#[derive(PartialEq, Eq)] +struct S { + a: T, + #[cfg(unix)] + b: String, + #[cfg(not(unix))] + b: Box, + #[cfg(feature = "feature1")] + c: Vec, +} + +impl S { + fn new(a: T) -> Self { + S { + a, + b: "hello world".to_string().into(), + #[cfg(feature = "feature1")] + c: vec![1, 1, 2, 3, 5, 8], + } + } +} + +#[test] +fn test_clone_S() { + let a = S::new(()); + assert_eq!(a.clone(), a); + assert!(a != S::default()); +} + +#[test] +fn test_debug_S() { + let s = S::new(42); + #[cfg(not(feature = "feature1"))] + let expected = "S { a: 42, b: \"hello world\" }"; + #[cfg(feature = "feature1")] + let expected = "S { a: 42, b: \"hello world\", c: [1, 1, 2, 3, 5, 8] }"; + assert_eq!(format!("{s:?}"), expected); +} + +#[autoimpl(Clone, Debug where T: trait)] +#[derive(PartialEq, Eq)] +enum E { + A(T), + #[cfg(unix)] + B(String), + #[cfg(not(unix))] + B(Box), + C { + #[cfg(feature = "feature1")] + nums: Vec, + }, +} + +#[test] +fn test_clone_E() { + let a = E::A(2.2); + assert_eq!(a.clone(), a); + + let b = E::<()>::B("rain".to_string().into()); + assert_eq!(b.clone(), b); + + let c = E::<()>::C { + #[cfg(feature = "feature1")] + nums: vec![1, 2, 3], + }; + assert_eq!(c.clone(), c); +}