From fc06db8f48b0a1646919ecf8979eedaf5a312538 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 3 Jun 2026 20:13:45 +0800 Subject: [PATCH 1/4] feat: add type-level traversal skip attributes --- README.md | 5 + traversable-derive/src/lib.rs | 25 +- traversable/src/lib.rs | 25 +- traversable/tests/test_derive_attributes.rs | 259 ++++++++++++++++++++ 4 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 traversable/tests/test_derive_attributes.rs diff --git a/README.md b/README.md index 12d974f..9b5c50b 100644 --- a/README.md +++ b/README.md @@ -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/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/src/lib.rs b/traversable/src/lib.rs index 4f35cde..2b5beeb 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); +} From 57ced743546a41c15dfa6a2d617185c496e65ca6 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 3 Jun 2026 20:18:55 +0800 Subject: [PATCH 2/4] fixup ci and lint Signed-off-by: tison --- .github/workflows/ci.yml | 16 +++++++++------- traversable/src/lib.rs | 12 ++++++------ xtask/src/main.rs | 3 ++- 3 files changed, 17 insertions(+), 14 deletions(-) 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/traversable/src/lib.rs b/traversable/src/lib.rs index 2b5beeb..cfbf76a 100644 --- a/traversable/src/lib.rs +++ b/traversable/src/lib.rs @@ -109,8 +109,8 @@ //! //! * `#[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. +//! * `#[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: //! @@ -283,8 +283,8 @@ pub trait VisitorMut { /// /// * `#[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. +/// * `#[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: /// @@ -360,8 +360,8 @@ pub trait Traversable: core::any::Any { /// /// * `#[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. +/// * `#[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: /// 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", From e4bf118e2fe4d8f813461082977e66500e9e27e5 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 3 Jun 2026 20:22:46 +0800 Subject: [PATCH 3/4] chore: prepare traversable 0.3.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- README.md | 2 +- traversable-derive/Cargo.toml | 2 +- traversable/Cargo.toml | 2 +- 6 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7aa6d49 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# CHANGELOG + +All notable changes to this project will be documented in this file. + +## Unreleased + +## [0.3.0] 2026-06-03 + +### 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)]` calls the visitor for the annotated struct or enum without traversing its children. + +### Documentation changes + +* Document the split between type-level traversal controls and field or variant `#[traverse(skip)]`. 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 9b5c50b..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`: 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/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" From 142ff1d8cb3280c5906ee831d779283cf426812a Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 3 Jun 2026 20:24:47 +0800 Subject: [PATCH 4/4] fixup changelog Signed-off-by: tison --- CHANGELOG.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa6d49..6fe5be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ All notable changes to this project will be documented in this file. ## Unreleased -## [0.3.0] 2026-06-03 - ### 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. @@ -13,8 +11,4 @@ All notable changes to this project will be documented in this file. ### New features * Add type-level `#[traverse(skip_self)]` and `#[traverse(skip_children)]` attributes for structs and enums. -* `#[traverse(skip_children)]` calls the visitor for the annotated struct or enum without traversing its children. - -### Documentation changes - -* Document the split between type-level traversal controls and field or variant `#[traverse(skip)]`. +* `#[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.