diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd756c9..e305158 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,28 +35,30 @@ concurrency: jobs: check: name: Check - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - uses: Swatinem/rust-cache@v2 + - name: Delete rust-toolchain.toml + run: rm rust-toolchain.toml - name: Install toolchain uses: dtolnay/rust-toolchain@nightly with: components: rustfmt,clippy - - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 with: tool: typos-cli,taplo-cli,hawkeye - - run: cargo +nightly x lint + - run: cargo x lint test: name: Run tests strategy: matrix: - os: [ ubuntu-22.04, macos-14, windows-2022 ] + os: [ ubuntu-24.04, macos-14, windows-2022 ] rust-version: [ "1.85.0", "stable" ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 - name: Delete rust-toolchain.toml run: rm rust-toolchain.toml @@ -70,7 +72,7 @@ jobs: required: name: Required - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: ${{ always() }} needs: - check diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6fe5be0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +## Unreleased + +### Breaking changes + +* Type-level `#[traverse(skip)]` is no longer supported. Use `#[traverse(skip_self)]` to skip calling the visitor for the annotated struct or enum while still traversing its children. + +### New features + +* Add type-level `#[traverse(skip_self)]` and `#[traverse(skip_children)]` attributes for structs and enums. +* `#[traverse(skip_children)]` skips traversing the children of the annotated struct or enum, but still calls the visitor for it, unless `#[traverse(skip_self)]` is also present. diff --git a/Cargo.lock b/Cargo.lock index f00295d..0080e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "traversable" -version = "0.2.0" +version = "0.3.0" dependencies = [ "ordered-float", "stacksafe", @@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "traversable-derive" -version = "0.1.0" +version = "0.2.0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1051dfb..11730b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ repository = "https://github.com/fast/traversable" rust-version = "1.85.0" [workspace.dependencies] -traversable-derive = { version = "=0.1.0", path = "traversable-derive" } +traversable-derive = { version = "=0.2.0", path = "traversable-derive" } [workspace.lints.rust] unknown_lints = "deny" diff --git a/README.md b/README.md index 12d974f..26b5a24 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Add `traversable` to your `Cargo.toml` with the `derive` feature: ```toml [dependencies] -traversable = { version = "0.2", features = ["derive", "std"] } +traversable = { version = "0.3", features = ["derive", "std"] } ``` Define your data structures and derive `Traversable`: @@ -89,6 +89,11 @@ fn main() { ## Attributes +The derive macro supports the following attributes on structs and enums: + +* `#[traverse(skip_self)]`: Skips calling the visitor for the annotated type while still traversing its children. +* `#[traverse(skip_children)]`: Calls the visitor for the annotated type without traversing its children. + The derive macro supports the following attributes on fields and variants: * `#[traverse(skip)]`: Skips traversing into the annotated field or variant. diff --git a/traversable-derive/Cargo.toml b/traversable-derive/Cargo.toml index fdc32df..34e51f9 100644 --- a/traversable-derive/Cargo.toml +++ b/traversable-derive/Cargo.toml @@ -14,7 +14,7 @@ [package] name = "traversable-derive" -version = "0.1.0" +version = "0.2.0" description = "Procedural macro to derive Traversable and TraversableMut" documentation = "https://docs.rs/traversable-derive" diff --git a/traversable-derive/src/lib.rs b/traversable-derive/src/lib.rs index 7b4a8a3..a3f67fc 100644 --- a/traversable-derive/src/lib.rs +++ b/traversable-derive/src/lib.rs @@ -205,10 +205,15 @@ fn resolve_crate_name() -> Path { fn impl_traversable(input: DeriveInput, mutable: bool) -> Result { let mut params = Params::from_attrs(input.attrs, "traverse")?; - params.validate(&["skip"])?; + params.validate(&["skip_self", "skip_children"])?; let skip_visit_self = params - .param("skip")? + .param("skip_self")? + .map(Param::unit) + .transpose()? + .is_some(); + let skip_children = params + .param("skip_children")? .map(Param::unit) .transpose()? .is_some(); @@ -250,8 +255,20 @@ fn impl_traversable(input: DeriveInput, mutable: bool) -> Result { }; let traverse_fields = match input.data { - Data::Struct(struct_) => traverse_struct(struct_, mutable), - Data::Enum(enum_) => traverse_enum(enum_, mutable), + Data::Struct(struct_) => { + if skip_children { + Ok(TokenStream::new()) + } else { + traverse_struct(struct_, mutable) + } + } + Data::Enum(enum_) => { + if skip_children { + Ok(TokenStream::new()) + } else { + traverse_enum(enum_, mutable) + } + } Data::Union(union_) => { return Err(Error::new_spanned( union_.union_token, diff --git a/traversable/Cargo.toml b/traversable/Cargo.toml index 0b16108..79cc5c3 100644 --- a/traversable/Cargo.toml +++ b/traversable/Cargo.toml @@ -14,7 +14,7 @@ [package] name = "traversable" -version = "0.2.0" +version = "0.3.0" description = "Visitor Pattern over Traversable data structures" documentation = "https://docs.rs/traversable" diff --git a/traversable/src/lib.rs b/traversable/src/lib.rs index 4f35cde..cfbf76a 100644 --- a/traversable/src/lib.rs +++ b/traversable/src/lib.rs @@ -105,6 +105,13 @@ //! //! ## Attributes //! +//! The derive macro supports the following attributes on structs and enums: +//! +//! * `#[traverse(skip_self)]`: Skips calling the visitor for the annotated type while still +//! traversing its children. +//! * `#[traverse(skip_children)]`: Calls the visitor for the annotated type without traversing its +//! children. +//! //! The derive macro supports the following attributes on fields and variants: //! //! * `#[traverse(skip)]`: Skips traversing into the annotated field or variant. @@ -272,7 +279,14 @@ pub trait VisitorMut { /// /// # Attributes /// -/// The derive macro supports the following attributes: +/// The derive macro supports the following attributes on structs and enums: +/// +/// * `#[traverse(skip_self)]`: Skips calling the visitor for the annotated type while still +/// traversing its children. +/// * `#[traverse(skip_children)]`: Calls the visitor for the annotated type without traversing its +/// children. +/// +/// The derive macro supports the following attributes on fields and variants: /// /// * `#[traverse(skip)]`: Skips traversing into the annotated field or variant. /// * `#[traverse(with = "function_name")]`: Uses a custom function to traverse the field. @@ -342,7 +356,14 @@ pub trait Traversable: core::any::Any { /// /// # Attributes /// -/// The derive macro supports the following attributes: +/// The derive macro supports the following attributes on structs and enums: +/// +/// * `#[traverse(skip_self)]`: Skips calling the visitor for the annotated type while still +/// traversing its children. +/// * `#[traverse(skip_children)]`: Calls the visitor for the annotated type without traversing its +/// children. +/// +/// The derive macro supports the following attributes on fields and variants: /// /// * `#[traverse(skip)]`: Skips traversing into the annotated field or variant. /// * `#[traverse(with = "function_name")]`: Uses a custom function to traverse the field. diff --git a/traversable/tests/test_derive_attributes.rs b/traversable/tests/test_derive_attributes.rs new file mode 100644 index 0000000..511d1d8 --- /dev/null +++ b/traversable/tests/test_derive_attributes.rs @@ -0,0 +1,259 @@ +// Copyright 2025 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg(all(feature = "std", feature = "derive"))] + +use std::any::Any; +use std::ops::ControlFlow; + +use traversable::Traversable; +use traversable::TraversableMut; +use traversable::Visitor; +use traversable::VisitorMut; + +#[derive(Traversable, TraversableMut)] +struct Child { + value: u64, +} + +#[derive(Traversable, TraversableMut)] +struct Parent { + child: Child, +} + +#[derive(Traversable, TraversableMut)] +#[traverse(skip_self)] +struct SkipSelfParent { + child: Child, +} + +#[derive(Traversable, TraversableMut)] +#[traverse(skip_children)] +#[allow(dead_code)] +struct SkipChildrenParent { + child: Child, +} + +#[derive(Traversable, TraversableMut)] +#[traverse(skip_self, skip_children)] +#[allow(dead_code)] +struct SkipSelfAndChildrenParent { + child: Child, +} + +#[derive(Traversable)] +#[traverse(skip_children)] +#[allow(dead_code)] +enum SkipChildrenEnum { + Child(Child), +} + +#[derive(Default)] +struct Counts { + parent_enter: usize, + parent_leave: usize, + child_enter: usize, + child_leave: usize, + skip_self_parent_enter: usize, + skip_self_parent_leave: usize, + skip_children_parent_enter: usize, + skip_children_parent_leave: usize, + skip_self_and_children_parent_enter: usize, + skip_self_and_children_parent_leave: usize, + skip_children_enum_enter: usize, + skip_children_enum_leave: usize, +} + +impl Visitor for Counts { + type Break = (); + + fn enter(&mut self, this: &dyn Any) -> ControlFlow { + if this.is::() { + self.parent_enter += 1; + } else if this.is::() { + self.child_enter += 1; + } else if this.is::() { + self.skip_self_parent_enter += 1; + } else if this.is::() { + self.skip_children_parent_enter += 1; + } else if this.is::() { + self.skip_self_and_children_parent_enter += 1; + } else if this.is::() { + self.skip_children_enum_enter += 1; + } + + ControlFlow::Continue(()) + } + + fn leave(&mut self, this: &dyn Any) -> ControlFlow { + if this.is::() { + self.parent_leave += 1; + } else if this.is::() { + self.child_leave += 1; + } else if this.is::() { + self.skip_self_parent_leave += 1; + } else if this.is::() { + self.skip_children_parent_leave += 1; + } else if this.is::() { + self.skip_self_and_children_parent_leave += 1; + } else if this.is::() { + self.skip_children_enum_leave += 1; + } + + ControlFlow::Continue(()) + } +} + +impl VisitorMut for Counts { + type Break = (); + + fn enter_mut(&mut self, this: &mut dyn Any) -> ControlFlow { + if this.is::() { + self.parent_enter += 1; + } else if this.is::() { + self.child_enter += 1; + } else if this.is::() { + self.skip_self_parent_enter += 1; + } else if this.is::() { + self.skip_children_parent_enter += 1; + } else if this.is::() { + self.skip_self_and_children_parent_enter += 1; + } + + ControlFlow::Continue(()) + } + + fn leave_mut(&mut self, this: &mut dyn Any) -> ControlFlow { + if this.is::() { + self.parent_leave += 1; + } else if this.is::() { + self.child_leave += 1; + } else if this.is::() { + self.skip_self_parent_leave += 1; + } else if this.is::() { + self.skip_children_parent_leave += 1; + } else if this.is::() { + self.skip_self_and_children_parent_leave += 1; + } + + ControlFlow::Continue(()) + } +} + +#[test] +fn traversable_visits_self_and_children_by_default() { + let parent = Parent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = parent.traverse(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.parent_enter, 1); + assert_eq!(counts.parent_leave, 1); + assert_eq!(counts.child_enter, 1); + assert_eq!(counts.child_leave, 1); +} + +#[test] +fn skip_self_visits_children_only() { + let parent = SkipSelfParent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = parent.traverse(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_self_parent_enter, 0); + assert_eq!(counts.skip_self_parent_leave, 0); + assert_eq!(counts.child_enter, 1); + assert_eq!(counts.child_leave, 1); +} + +#[test] +fn skip_children_visits_self_only() { + let parent = SkipChildrenParent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = parent.traverse(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_children_parent_enter, 1); + assert_eq!(counts.skip_children_parent_leave, 1); + assert_eq!(counts.child_enter, 0); + assert_eq!(counts.child_leave, 0); +} + +#[test] +fn skip_self_and_children_visits_nothing() { + let parent = SkipSelfAndChildrenParent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = parent.traverse(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_self_and_children_parent_enter, 0); + assert_eq!(counts.skip_self_and_children_parent_leave, 0); + assert_eq!(counts.child_enter, 0); + assert_eq!(counts.child_leave, 0); +} + +#[test] +fn skip_children_visits_enum_self_only() { + let item = SkipChildrenEnum::Child(Child { value: 1 }); + let mut counts = Counts::default(); + + let result = item.traverse(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_children_enum_enter, 1); + assert_eq!(counts.skip_children_enum_leave, 1); + assert_eq!(counts.child_enter, 0); + assert_eq!(counts.child_leave, 0); +} + +#[test] +fn traversable_mut_honors_type_level_attributes() { + let mut skip_self = SkipSelfParent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = skip_self.traverse_mut(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_self_parent_enter, 0); + assert_eq!(counts.skip_self_parent_leave, 0); + assert_eq!(counts.child_enter, 1); + assert_eq!(counts.child_leave, 1); + + let mut skip_children = SkipChildrenParent { + child: Child { value: 1 }, + }; + let mut counts = Counts::default(); + + let result = skip_children.traverse_mut(&mut counts); + + assert!(result.is_continue()); + assert_eq!(counts.skip_children_parent_enter, 1); + assert_eq!(counts.skip_children_parent_leave, 1); + assert_eq!(counts.child_enter, 0); + assert_eq!(counts.child_leave, 0); +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 9ee7173..fe3e07e 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -152,7 +152,7 @@ fn make_test_cmd(no_capture: bool, features: &[&str]) -> StdCommand { fn make_format_cmd(fix: bool) -> StdCommand { let mut cmd = find_command("cargo"); - cmd.args(["fmt", "--all"]); + cmd.args(["+nightly", "fmt", "--all"]); if !fix { cmd.arg("--check"); } @@ -162,6 +162,7 @@ fn make_format_cmd(fix: bool) -> StdCommand { fn make_clippy_cmd(fix: bool) -> StdCommand { let mut cmd = find_command("cargo"); cmd.args([ + "+nightly", "clippy", "--tests", "--all-features",