├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── release.yml │ ├── check.yml │ └── test.yml ├── example ├── Cargo.toml ├── README.md ├── CHANGELOG.md └── src │ └── lib.rs ├── Cargo.toml ├── LICENSE.md ├── README.md ├── CHANGELOG.md ├── cliff.toml └── src ├── item_like.rs ├── lib.rs ├── stable.rs └── unstable.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 4 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | 8 | [*.{yaml,yml}] 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # see https://release-plz.ieni.dev/docs/github 4 | # for more information 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: write 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | release-plz: 17 | name: Release-plz 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Install Rust stable 25 | uses: dtolnay/rust-toolchain@stable 26 | - name: Run release-plz 27 | uses: MarcoIeni/release-plz-action@v0.5 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 31 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "instability-example" 3 | description = "Example crate demonstrating instablity usage." 4 | documentation = "https://docs.rs/instability-example/" 5 | authors.workspace = true 6 | license.workspace = true 7 | version.workspace = true 8 | edition.workspace = true 9 | rust-version.workspace = true 10 | repository.workspace = true 11 | 12 | [features] 13 | default = [] 14 | unstable-enum = [] 15 | unstable-constant = [] 16 | unstable-function = [] 17 | unstable-method = [] 18 | unstable-module = [] 19 | unstable-private-function = [] 20 | unstable-reexport = [] 21 | unstable-static = [] 22 | unstable-struct-with-issue = [] 23 | unstable-struct = [] 24 | unstable-trait = [] 25 | unstable-type-alias = [] 26 | 27 | [dependencies] 28 | instability.workspace = true 29 | 30 | [package.metadata.docs.rs] 31 | all-features = true 32 | rustdoc-args = ["--cfg", "docsrs"] 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["example"] 4 | 5 | [workspace.package] 6 | authors = ["Stephen M. Coakley ", "The Ratatui Developers"] 7 | license = "MIT" 8 | version = "0.3.7" 9 | edition = "2021" 10 | rust-version = "1.64" 11 | repository = "https://github.com/ratatui-org/instability" 12 | 13 | [workspace.dependencies] 14 | instability = { path = ".", version = "0.3.7" } 15 | 16 | [package] 17 | name = "instability" 18 | description = "Rust API stability attributes for the rest of us. A fork of the `stability` crate." 19 | documentation = "https://docs.rs/instability/" 20 | authors.workspace = true 21 | license.workspace = true 22 | version.workspace = true 23 | edition.workspace = true 24 | rust-version.workspace = true 25 | repository.workspace = true 26 | readme = "README.md" 27 | 28 | [dependencies] 29 | darling = "0.20.10" 30 | indoc = "2.0.5" 31 | proc-macro2 = "1.0.92" 32 | quote = "1.0.37" 33 | syn = { version = "2.0.90", features = ["derive", "full"] } 34 | 35 | [dev-dependencies] 36 | pretty_assertions = "1.4.1" 37 | 38 | [lib] 39 | proc-macro = true 40 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Instability-example 2 | 3 | This crate is an example of using the stability attributes from the [instability crate]. 4 | 5 | [![Crate Badge]][Crate] [![Build Badge]][Build] [![Docs Badge]][Docs] [![License Badge]][License] 6 | ![MSRV Badge] 7 | 8 | ## Overview 9 | 10 | - Example code: [lib.rs](./src/lib.rs). 11 | - Output: See the [Docs.rs][Docs] page for the output of the example code. 12 | 13 | ## License 14 | 15 | This project's source code and documentation are licensed under the MIT [License]. 16 | 17 | [instability crate]: https://crates.io/crates/instability 18 | [Crate Badge]: https://img.shields.io/crates/v/instability-example 19 | [Build Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/instability/check.yml 20 | [Docs Badge]: https://img.shields.io/docsrs/instability-example 21 | [License Badge]: https://img.shields.io/crates/l/instability-example 22 | [MSRV Badge]: https://img.shields.io/crates/msrv/instability-example 23 | [Crate]: https://crates.io/crates/instability-example 24 | [Build]: https://github.com/ratatui/instability/actions/workflows/check.yml 25 | [Docs]: https://docs.rs/instability-example 26 | [License]: ../LICENSE.md 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Stephen M. Coakley 4 | Copyright (c) The Ratatui Developers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.7](https://github.com/ratatui/instability/compare/instability-example-v0.3.6...instability-example-v0.3.7) - 2025-01-10 11 | 12 | ### Other 13 | 14 | - Add #[allow(unused_imports)] lint to unstable reexports ([#21](https://github.com/ratatui/instability/pull/21)) 15 | 16 | ## [0.3.5](https://github.com/ratatui/instability/compare/instability-example-v0.3.4...instability-example-v0.3.5) - 2024-12-21 17 | 18 | ### Added 19 | 20 | - Allow marking impl blocks unstable/stable (#15) 21 | - add stable macro (#14) 22 | - use doc(cfg) 23 | - allow use statements to be marked unstable (#3) 24 | 25 | ### Other 26 | 27 | - prepare instability-example for publish (#18) 28 | - bump msrv to 1.63 29 | - tweak doc wording and formatting (#4) 30 | - fork and change name to instabilty 31 | - Apply visibility restriction to struct fields ([#10](https://github.com/ratatui/instability/pull/10)) 32 | - Update to Rust edition 2021 ([#9](https://github.com/ratatui/instability/pull/9)) 33 | - Add issue tracking 34 | - Initial version 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Instability 2 | 3 | Rust API stability attributes for the rest of us. 4 | 5 | [![Crate Badge]][Crate] [![Build Badge]][Build] [![Docs Badge]][Docs] [![License Badge]][License] 6 | ![MSRV Badge] 7 | 8 | ## Overview 9 | 10 | This crate provides attribute macros for specifying API stability of public API items of a crate. It 11 | is a [fork] of the [Stability] original created by Stephen M. Coakley ([@sagebind]). 12 | 13 | ## Usage 14 | 15 | Add the `instability` crate to your `Cargo.toml` file: 16 | 17 | ```shell 18 | cargo add instability 19 | ``` 20 | 21 | Then, use the `#[instability::stable]` and `#[instability::unstable]` attributes to specify the 22 | stability of your API items: 23 | 24 | ```rust 25 | /// This function does something really risky! 26 | #[instability::unstable(feature = "risky-function")] 27 | pub fn risky_function() { 28 | println!("This function is unstable!"); 29 | } 30 | 31 | /// This function is safe to use! 32 | #[instability::stable(since = "1.0.0")] 33 | pub fn stable_function() { 34 | println!("This function is stable!"); 35 | } 36 | ``` 37 | 38 | A feature flag prefixed with "unstable-" will be created that can be used to enable unstable items. 39 | The macro will append an extra documentation comment that describes the stability of the item. The 40 | visibility of the item will be changed to `pub(crate)` when the feature is not enabled (or when the 41 | attribute is on an impl block, the entire block will be removed). 42 | 43 | Check out the [Docs] for detailed usage. See [instability-example] for a complete example. 44 | 45 | ## MSRV 46 | 47 | The minimum supported Rust version (MSRV) is 1.64.0. 48 | 49 | ## License 50 | 51 | This project's source code and documentation are licensed under the MIT [License]. 52 | 53 | [Crate Badge]: https://img.shields.io/crates/v/instability 54 | [Build Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/instability/check.yml 55 | [Docs Badge]: https://img.shields.io/docsrs/instability 56 | [License Badge]: https://img.shields.io/crates/l/instability 57 | [MSRV Badge]: https://img.shields.io/crates/msrv/instability 58 | [Crate]: https://crates.io/crates/instability 59 | [Build]: https://github.com/ratatui/instability/actions/workflows/check.yml 60 | [Docs]: https://docs.rs/instability 61 | [License]: ./LICENSE.md 62 | [stability]: https://crates.io/crates/stability 63 | [@Sagebind]: https://github.com/sagebind 64 | [fork]: https://github.com/sagebind/stability/issues/12 65 | [instability-example]: https://crates.io/crates/instability-example 66 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel 13 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | rustfmt: 20 | name: Rustfmt 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Install Rust stable 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | components: rustfmt 29 | - name: Run cargo fmt 30 | run: cargo fmt -- --check 31 | - name: Cache Cargo dependencies 32 | uses: Swatinem/rust-cache@v2 33 | clippy: 34 | name: Clippy (${{ matrix.toolchain }}) 35 | runs-on: ubuntu-latest 36 | permissions: 37 | checks: write 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | # Get early warnings about new lints introduced in the beta channel 42 | toolchain: [stable, beta] 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | - name: Install Rust stable 47 | uses: dtolnay/rust-toolchain@stable 48 | with: 49 | components: clippy 50 | - name: Run clippy action 51 | uses: clechasseur/rs-clippy-check@v3 52 | - name: Cache Cargo dependencies 53 | uses: Swatinem/rust-cache@v2 54 | docs: 55 | # run docs generation on nightly rather than stable. This enables features like 56 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 57 | # API be documented as only available in some specific platforms. 58 | name: Check docs 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Install Rust nightly 63 | uses: dtolnay/rust-toolchain@nightly 64 | - name: Run cargo doc 65 | run: cargo doc --no-deps --all-features 66 | env: 67 | RUSTDOCFLAGS: --cfg docsrs 68 | msrv: 69 | # check that we can build using the minimal rust version that is specified by this crate 70 | name: Check MSRV (1.64) 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: dtolnay/rust-toolchain@1.64 75 | - run: cargo check 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Unreleased 6 | 7 | ## [0.3.7](https://github.com/ratatui/instability/compare/instability-v0.3.6...instability-v0.3.7) - 2025-01-10 8 | 9 | ### Other 10 | 11 | - Add #[allow(unused_imports)] lint to unstable reexports ([#21](https://github.com/ratatui/instability/pull/21)) 12 | 13 | ## [0.3.6](https://github.com/ratatui/instability/compare/instability-v0.3.5...instability-v0.3.6) - 2025-01-04 14 | 15 | ### Other 16 | 17 | - Move pretty_assertions to dev-dependencies ([#19](https://github.com/ratatui/instability/pull/19)) 18 | 19 | ## [0.3.5](https://github.com/ratatui/instability/compare/instability-v0.3.4...instability-v0.3.5) - 2024-12-21 20 | 21 | ### Other 22 | 23 | - prepare instability-example for publish (#18) 24 | - clippy --fix 25 | 26 | ## [0.3.4](https://github.com/ratatui/instability/compare/instability-v0.3.3...instability-v0.3.4) - 2024-12-21 27 | 28 | ### Added 29 | 30 | - Allow marking impl blocks unstable/stable ([#15](https://github.com/ratatui/instability/pull/15)) 31 | 32 | ## [0.3.3](https://github.com/ratatui/instability/compare/instability-v0.3.2...instability-v0.3.3) - 2024-11-12 33 | 34 | ### Added 35 | 36 | - add stable macro ([#14](https://github.com/ratatui/instability/pull/14)) 37 | - use doc(cfg) 38 | 39 | ### Fixed 40 | 41 | - tests ([#13](https://github.com/ratatui/instability/pull/13)) 42 | - change master to main in lib.rs 43 | 44 | ### Other 45 | 46 | - bump msrv to 1.63 47 | - use proc_macro2 and add tests 48 | - use darling instead of manual parsing for better error messages on attributes 49 | 50 | ## [0.3.2](https://github.com/ratatui-org/instability/compare/instability-v0.3.1...instability-v0.3.2) - 2024-06-27 51 | 52 | ### Fixed 53 | 54 | - readme link to license ([#7](https://github.com/ratatui-org/instability/pull/7)) 55 | - readme badge ([#5](https://github.com/ratatui-org/instability/pull/5)) 56 | 57 | ## [0.3.1](https://github.com/ratatui-org/instability/compare/instability-v0.3.0...instability-v0.3.1) - 2024-06-27 58 | 59 | ### Added 60 | 61 | - allow use statements to be marked unstable ([#3](https://github.com/ratatui-org/instability/pull/3)) 62 | 63 | ### Other 64 | 65 | - tweak doc wording and formatting ([#4](https://github.com/ratatui-org/instability/pull/4)) 66 | - add release automation ([#1](https://github.com/ratatui-org/instability/pull/1)) 67 | 68 | ## [0.3.0] - 2024-06-27 69 | 70 | ### ⚙️ Miscellaneous Tasks 71 | 72 | - Fork and change name to instabilty 73 | 74 | ## [0.2.0] - 2024-04-02 75 | 76 | ### 🚜 Refactor 77 | 78 | - Migrate to syn 2.x ([#8](https://github.com/ratatui-org/instability/issues/8)) 79 | 80 | ## [0.1.1] - 2022-01-11 81 | 82 | ### 🚀 Features 83 | 84 | - Support `const`, `static` and `type` 85 | 86 | ## [0.1.0] - 2020-10-23 87 | 88 | 89 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # template for the changelog footer 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | striptags | trim | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 26 | {% if commit.breaking %}[**breaking**] {% endif %}\ 27 | {{ commit.message | upper_first }}\ 28 | {% endfor %} 29 | {% endfor %}\n 30 | """ 31 | # template for the changelog footer 32 | footer = """ 33 | 34 | """ 35 | # remove the leading and trailing s 36 | trim = true 37 | # postprocessors 38 | postprocessors = [ 39 | { pattern = '', replace = "https://github.com/ratatui-org/instability" }, # replace repository URL 40 | ] 41 | 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = false 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # Replace issue numbers 52 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, 53 | # Check spelling of the commit with https://github.com/crate-ci/typos 54 | # If the spelling is incorrect, it will be automatically fixed. 55 | # { pattern = '.*', replace_command = 'typos --write-changes -' }, 56 | ] 57 | # regex for parsing and grouping commits 58 | commit_parsers = [ 59 | { message = "^feat", group = "🚀 Features" }, 60 | { message = "^fix", group = "🐛 Bug Fixes" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { message = "^perf", group = "⚡ Performance" }, 63 | { message = "^refactor", group = "🚜 Refactor" }, 64 | { message = "^style", group = "🎨 Styling" }, 65 | { message = "^test", group = "🧪 Testing" }, 66 | { message = "^chore\\(release\\): prepare for", skip = true }, 67 | # { message = "^chore\\(deps.*\\)", skip = true }, 68 | { message = "^chore\\(pr\\)", skip = true }, 69 | { message = "^chore\\(pull\\)", skip = true }, 70 | { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, 71 | { body = ".*security", group = "🛡️ Security" }, 72 | { message = "^revert", group = "◀️ Revert" }, 73 | ] 74 | # protect breaking changes from being skipped due to matching a skipping commit_parser 75 | protect_breaking_commits = false 76 | # filter out the commits that are not matched by commit parsers 77 | filter_commits = false 78 | # regex for matching git tags 79 | # tag_pattern = "v[0-9].*" 80 | # regex for skipping tags 81 | # skip_tags = "" 82 | # regex for ignoring tags 83 | # ignore_tags = "" 84 | # sort the tags topologically 85 | topo_order = false 86 | # sort the commits inside sections by oldest/newest order 87 | sort_commits = "oldest" 88 | # limit the number of commits included in the changelog. 89 | # limit_commits = 42 90 | -------------------------------------------------------------------------------- /src/item_like.rs: -------------------------------------------------------------------------------- 1 | use syn::Visibility; 2 | 3 | pub trait Stability { 4 | #[allow(unused)] 5 | fn attrs(&self) -> &[syn::Attribute]; 6 | 7 | fn push_attr(&mut self, attr: syn::Attribute); 8 | } 9 | 10 | pub trait ItemLike: Stability { 11 | fn visibility(&self) -> &Visibility; 12 | 13 | fn set_visibility(&mut self, visibility: Visibility); 14 | 15 | fn is_public(&self) -> bool { 16 | matches!(self.visibility(), Visibility::Public(_)) 17 | } 18 | 19 | fn allowed_lints(&self) -> Vec; 20 | } 21 | 22 | /// Implement `ItemLike` for the given type. 23 | /// 24 | /// This makes each of the syn::Item* types implement our `ItemLike` trait to make it possible to 25 | /// work with them in a more uniform way. 26 | /// 27 | /// A single type can be passed to this macro, or multiple types can be passed at once. 28 | /// Each type can be passed with a list of lints that are allowed for that type (defaulting to 29 | /// `dead_code` if not specified). 30 | macro_rules! impl_item_like { 31 | // run impl_item_like for each item in a list of items 32 | ($($(#[allow($($lint:ident),*)])? $ty:ty ),+ ,) => { 33 | $( 34 | impl_item_like!($(#[allow($($lint),*)])? $ty ); 35 | )* 36 | }; 37 | 38 | // run impl_item_like for a single item without any lints 39 | ($ty:ty) => { 40 | impl_item_like!(#[allow(dead_code)] $ty ); 41 | }; 42 | 43 | // Implement `ItemLike` for the given type. 44 | (#[allow($($lint:ident),*)] $ty:ty) => { 45 | impl Stability for $ty { 46 | fn attrs(&self) -> &[syn::Attribute] { 47 | &self.attrs 48 | } 49 | 50 | fn push_attr(&mut self, attr: syn::Attribute) { 51 | self.attrs.push(attr); 52 | } 53 | } 54 | 55 | impl ItemLike for $ty { 56 | fn visibility(&self) -> &Visibility { 57 | &self.vis 58 | } 59 | 60 | fn set_visibility(&mut self, visibility: Visibility) { 61 | self.vis = visibility; 62 | } 63 | 64 | fn allowed_lints(&self) -> Vec { 65 | vec![ 66 | $(syn::Ident::new(stringify!($lint), proc_macro2::Span::call_site()),)* 67 | ] 68 | } 69 | } 70 | }; 71 | 72 | } 73 | 74 | impl_item_like!( 75 | syn::ItemType, 76 | syn::ItemEnum, 77 | syn::ItemFn, 78 | syn::ItemMod, 79 | syn::ItemTrait, 80 | syn::ItemConst, 81 | syn::ItemStatic, 82 | #[allow(unused_imports)] 83 | syn::ItemUse, 84 | ); 85 | 86 | impl Stability for syn::ItemStruct { 87 | fn attrs(&self) -> &[syn::Attribute] { 88 | &self.attrs 89 | } 90 | 91 | fn push_attr(&mut self, attr: syn::Attribute) { 92 | self.attrs.push(attr); 93 | } 94 | } 95 | 96 | impl ItemLike for syn::ItemStruct { 97 | fn visibility(&self) -> &Visibility { 98 | &self.vis 99 | } 100 | 101 | fn set_visibility(&mut self, visibility: Visibility) { 102 | // Also constrain visibility of all fields to be at most the given 103 | // item visibility. 104 | self.fields 105 | .iter_mut() 106 | .filter(|field| matches!(&field.vis, Visibility::Public(_))) 107 | .for_each(|field| field.vis = visibility.clone()); 108 | 109 | self.vis = visibility; 110 | } 111 | 112 | fn allowed_lints(&self) -> Vec { 113 | vec![syn::Ident::new("dead_code", proc_macro2::Span::call_site())] 114 | } 115 | } 116 | 117 | impl Stability for syn::ItemImpl { 118 | fn attrs(&self) -> &[syn::Attribute] { 119 | &self.attrs 120 | } 121 | 122 | fn push_attr(&mut self, attr: syn::Attribute) { 123 | self.attrs.push(attr); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # This is the main CI workflow that runs the test suite on all pushes to main and all pull requests. 4 | # It runs the following jobs: 5 | # - required: runs the test suite on ubuntu with stable and beta rust toolchains 6 | # - minimal: runs the test suite with the minimal versions of the dependencies that satisfy the 7 | # requirements of this crate, and its dependencies 8 | # - os-check: runs the test suite on mac and windows 9 | # - coverage: runs the test suite and collects coverage information 10 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 11 | on: 12 | push: 13 | branches: 14 | - main 15 | pull_request: 16 | 17 | # ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel 18 | # and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | required: 25 | runs-on: ubuntu-latest 26 | name: ubuntu (${{ matrix.toolchain }}) 27 | strategy: 28 | matrix: 29 | # run on stable and beta to ensure that tests won't break on the next version of the rust 30 | # toolchain 31 | toolchain: [stable, beta] 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Install Rust ${{ matrix.toolchain }} 35 | uses: dtolnay/rust-toolchain@master 36 | with: 37 | toolchain: ${{ matrix.toolchain }} 38 | # enable this ci template to run regardless of whether the lockfile is checked in or not 39 | - name: cargo generate-lockfile 40 | if: hashFiles('Cargo.lock') == '' 41 | run: cargo generate-lockfile 42 | - name: cargo test --locked 43 | run: cargo test --locked --all-features --all-targets 44 | - name: cargo test --doc 45 | run: cargo test --locked --all-features --doc 46 | minimal-versions: 47 | # This action chooses the oldest version of the dependencies permitted by Cargo.toml to ensure 48 | # that this crate is compatible with the minimal version that this crate and its dependencies 49 | # require. This will pickup issues where this create relies on functionality that was introduced 50 | # later than the actual version specified (e.g., when we choose just a major version, but a 51 | # method was added after this version). 52 | # 53 | # This particular check can be difficult to get to succeed as often transitive dependencies may 54 | # be incorrectly specified (e.g., a dependency specifies 1.0 but really requires 1.1.5). There 55 | # is an alternative flag available -Zdirect-minimal-versions that uses the minimal versions for 56 | # direct dependencies of this crate, while selecting the maximal versions for the transitive 57 | # dependencies. Alternatively, you can add a line in your Cargo.toml to artificially increase 58 | # the minimal dependency, which you do with e.g.: 59 | # ```toml 60 | # # for minimal-versions 61 | # [target.'cfg(any())'.dependencies] 62 | # openssl = { version = "0.10.55", optional = true } # needed to allow foo to build with -Zminimal-versions 63 | # ``` 64 | # The optional = true is necessary in case that dependency isn't otherwise transitively required 65 | # by your library, and the target bit is so that this dependency edge never actually affects 66 | # Cargo build order. See also 67 | # https://github.com/jonhoo/fantoccini/blob/fde336472b712bc7ebf5b4e772023a7ba71b2262/Cargo.toml#L47-L49. 68 | # This action is run on ubuntu with the stable toolchain, as it is not expected to fail 69 | runs-on: ubuntu-latest 70 | name: minimal-versions 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Install Rust stable 74 | uses: dtolnay/rust-toolchain@stable 75 | - name: Install Rust nightly for -Zdirect-minimal-versions 76 | uses: dtolnay/rust-toolchain@nightly 77 | - name: rustup default stable 78 | run: rustup default stable 79 | - name: cargo update -Zdirect-minimal-versions 80 | run: cargo +nightly update -Zdirect-minimal-versions 81 | - name: cargo test 82 | run: cargo test --locked --all-features --all-targets 83 | - name: Cache Cargo dependencies 84 | uses: Swatinem/rust-cache@v2 85 | os-check: 86 | # run cargo test on mac and windows 87 | runs-on: ${{ matrix.os }} 88 | name: ${{ matrix.os }} (stable) 89 | strategy: 90 | fail-fast: false 91 | matrix: 92 | os: [macos-latest, windows-latest] 93 | steps: 94 | - name: Checkout 95 | uses: actions/checkout@v4 96 | - name: Install Rust stable 97 | uses: dtolnay/rust-toolchain@stable 98 | - name: cargo generate-lockfile 99 | if: hashFiles('Cargo.lock') == '' 100 | run: cargo generate-lockfile 101 | - name: cargo test 102 | run: cargo test --locked --all-features --all-targets 103 | - name: Cache Cargo dependencies 104 | uses: Swatinem/rust-cache@v2 105 | coverage: 106 | # use llvm-cov to build and collect coverage and outputs in a format that 107 | # is compatible with codecov.io 108 | # 109 | # note that codecov as of v4 requires that CODECOV_TOKEN from 110 | # 111 | # https://app.codecov.io/gh///settings 112 | # 113 | # is set in two places on your repo: 114 | # 115 | # - https://github.com/jonhoo/guardian/settings/secrets/actions 116 | # - https://github.com/jonhoo/guardian/settings/secrets/dependabot 117 | # 118 | # (the former is needed for codecov uploads to work with Dependabot PRs) 119 | # 120 | # PRs coming from forks of your repo will not have access to the token, but 121 | # for those, codecov allows uploading coverage reports without a token. 122 | # it's all a little weird and inconvenient. see 123 | # 124 | # https://github.com/codecov/feedback/issues/112 125 | # 126 | # for lots of more discussion 127 | runs-on: ubuntu-latest 128 | name: coverage (stable) 129 | steps: 130 | - name: Checkout 131 | uses: actions/checkout@v4 132 | - name: Install Rust stable 133 | uses: dtolnay/rust-toolchain@stable 134 | with: 135 | components: llvm-tools-preview 136 | - name: cargo install cargo-llvm-cov 137 | uses: taiki-e/install-action@cargo-llvm-cov 138 | - name: cargo generate-lockfile 139 | if: hashFiles('Cargo.lock') == '' 140 | run: cargo generate-lockfile 141 | - name: cargo llvm-cov 142 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info 143 | - name: Record Rust version 144 | run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" 145 | - name: Cache Cargo dependencies 146 | uses: Swatinem/rust-cache@v2 147 | - name: Upload to codecov.io 148 | uses: codecov/codecov-action@v4 149 | with: 150 | fail_ci_if_error: true 151 | token: ${{ secrets.CODECOV_TOKEN }} 152 | env_vars: OS,RUST 153 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | #![allow(dead_code)] 3 | 4 | //! This is an example library demonstrating various attributes from the [`instability`] crate. 5 | 6 | /// A stable type alias 7 | /// 8 | /// This type alias is stable 9 | #[instability::stable(since = "v1.0.0")] 10 | pub type StableTypeAlias = u8; 11 | 12 | /// An unstable type alias 13 | /// 14 | /// This type alias is unstable 15 | #[instability::unstable(feature = "type-alias")] 16 | pub type UnstableTypeAlias = u8; 17 | 18 | /// A stable constant 19 | /// 20 | /// This constant is stable 21 | #[instability::stable(since = "v1.0.0")] 22 | pub const STABLE_CONSTANT: u8 = 42; 23 | 24 | /// An unstable constant 25 | /// 26 | /// This constant is unstable 27 | #[instability::unstable(feature = "constant")] 28 | pub const UNSTABLE_CONSTANT: u8 = 42; 29 | 30 | /// A stable static 31 | /// 32 | /// This static is stable 33 | #[instability::stable(since = "v1.0.0")] 34 | pub static STABLE_STATIC: u8 = 42; 35 | 36 | /// An unstable static 37 | /// 38 | /// This static is unstable 39 | #[instability::unstable(feature = "static")] 40 | pub static UNSTABLE_STATIC: u8 = 42; 41 | 42 | /// A stable function 43 | /// 44 | /// This function is stable 45 | #[instability::stable(since = "v1.0.0")] 46 | pub fn stable_function() { 47 | unimplemented!() 48 | } 49 | 50 | /// An unstable function 51 | /// 52 | /// This function is unstable 53 | #[instability::unstable(feature = "function")] 54 | pub fn unstable_function() { 55 | unimplemented!() 56 | } 57 | 58 | /// A stable struct 59 | /// 60 | /// This struct is stable 61 | #[instability::stable(since = "v1.0.0")] 62 | pub struct StableStruct { 63 | pub x: u8, 64 | } 65 | 66 | impl StableStruct { 67 | /// An unstable method 68 | /// 69 | /// This method is unstable 70 | #[instability::unstable(feature = "method")] 71 | pub fn unstable_method(&self) { 72 | unimplemented!() 73 | } 74 | 75 | /// A stable method 76 | /// 77 | /// This method is stable 78 | #[instability::stable(since = "v1.0.0")] 79 | pub fn stable_method(&self) { 80 | unimplemented!() 81 | } 82 | } 83 | 84 | /// An unstable struct 85 | /// 86 | /// This struct is unstable 87 | #[instability::unstable(feature = "struct")] 88 | pub struct UnstableStruct { 89 | pub x: u8, 90 | } 91 | 92 | impl UnstableStruct { 93 | /// An unstable method 94 | /// 95 | /// This method is unstable 96 | #[instability::unstable(feature = "method")] 97 | pub fn unstable_method(&self) { 98 | unimplemented!() 99 | } 100 | 101 | /// A stable method 102 | /// 103 | /// This method is stable 104 | #[allow( 105 | unreachable_pub, 106 | // reason = "The unstable macros cannot make the method pub(crate)" 107 | )] 108 | #[instability::stable(since = "v1.0.0")] 109 | pub fn stable_method(&self) { 110 | unimplemented!() 111 | } 112 | } 113 | 114 | /// An unstable struct with an issue link 115 | /// 116 | /// This struct is unstable and has an issue link. 117 | #[instability::unstable(feature = "struct-with-issue", issue = "#123")] 118 | pub struct UnstableStructWithIssue { 119 | pub x: u8, 120 | } 121 | 122 | /// A stable trait 123 | /// 124 | /// This trait is stable 125 | #[instability::stable(since = "v1.0.0")] 126 | pub trait StableTrait { 127 | /// A stable trait method 128 | /// 129 | /// This method is stable. 130 | fn stable_trait_method(&self) { 131 | unimplemented!() 132 | } 133 | 134 | // Not yet supported 135 | // /// An unstable trait method 136 | // /// 137 | // /// This method is unstable. 138 | // #[instability::unstable(feature = "trait-method")] 139 | // fn unstable_trait_method(&self); 140 | } 141 | 142 | #[instability::stable(since = "v1.0.0")] 143 | impl StableTrait for StableStruct {} 144 | 145 | /// An unstable trait 146 | /// 147 | /// This trait is unstable 148 | #[instability::unstable(feature = "trait")] 149 | pub trait UnstableTrait { 150 | /// A stable trait method 151 | /// 152 | /// This method is stable. 153 | fn stable_trait_method(&self) { 154 | unimplemented!() 155 | } 156 | 157 | // Not yet supported 158 | // /// An unstable trait method 159 | // /// 160 | // /// This method is not implemented yet. 161 | // #[instability::unstable(feature = "trait-method")] 162 | // fn unstable_trait_method(&self); 163 | } 164 | 165 | #[instability::unstable(feature = "trait")] 166 | impl UnstableTrait for StableStruct {} 167 | 168 | /// A stable enum 169 | /// 170 | /// This enum is stable. 171 | #[instability::stable(since = "v1.0.0")] 172 | pub enum StableEnum { 173 | /// An enum variant 174 | /// 175 | /// This variant is stable. 176 | Variant, 177 | } 178 | 179 | /// An unstable enum 180 | /// 181 | /// This enum is unstable. 182 | #[instability::unstable(feature = "enum")] 183 | pub enum UnstableEnum { 184 | /// An enum variant 185 | /// 186 | /// This variant is stable. 187 | Variant, 188 | // Not yet supported 189 | // /// An unstable enum variant 190 | // /// 191 | // /// This variant is not implemented yet. 192 | // #[instability::unstable(feature = "enum-variant")] 193 | // UnstableVariant, 194 | } 195 | 196 | /// A stable module 197 | /// 198 | /// This module is stable. 199 | #[instability::stable(since = "v1.0.0")] 200 | pub mod stable { 201 | /// A stable function 202 | /// 203 | /// This function is stable. 204 | pub fn stable_function() { 205 | unimplemented!() 206 | } 207 | 208 | /// An unstable function 209 | /// 210 | /// This function is unstable. 211 | #[instability::unstable(feature = "function")] 212 | pub fn unstable_function() { 213 | unimplemented!() 214 | } 215 | } 216 | 217 | /// An unstable module 218 | /// 219 | /// This module is unstable. 220 | #[instability::unstable(feature = "module")] 221 | pub mod unstable { 222 | /// A stable function 223 | /// 224 | /// This function is stable. 225 | #[instability::stable(since = "v1.0.0")] 226 | pub fn stable_function() { 227 | unimplemented!() 228 | } 229 | 230 | /// An unstable function 231 | /// 232 | /// This function is unstable. 233 | #[instability::unstable(feature = "function")] 234 | pub fn unstable_function() { 235 | unimplemented!() 236 | } 237 | } 238 | 239 | /// A private module 240 | /// 241 | /// This module is private. 242 | mod private { 243 | /// A private function 244 | /// 245 | /// This function is private. 246 | pub fn private_function() { 247 | unimplemented!() 248 | } 249 | 250 | /// An unstable private function 251 | /// 252 | /// This function is unstable. 253 | #[instability::unstable(feature = "private-function")] 254 | pub fn unstable_private_function() { 255 | unimplemented!() 256 | } 257 | } 258 | 259 | /// A stable re-export of a private stable item 260 | /// 261 | /// This re-export is stable. 262 | pub use private::private_function as stable_reexport; 263 | 264 | /// An unstable re-export of a private stable item 265 | /// 266 | /// This re-export is unstable. 267 | #[instability::unstable(feature = "reexport")] 268 | pub use private::private_function as unstable_reexport; 269 | 270 | // This does not work as the unstable_private_function is only public within the crate and cannot 271 | // be re-exported 272 | // /// A stable reexport of a private unstable item 273 | // /// 274 | // /// This export is stable. 275 | // pub use private::unstable_private_function as stable_unstable_reexport; 276 | 277 | /// An unstable reexport of a private unstable item 278 | /// 279 | /// This export is unstable. The availability section on this will be appended to the availability 280 | /// section of the unstable_private_function, which will look odd. Consider avoiding re-exporting 281 | /// unstable items like this, and instead only mark the re-export itself as unstable. 282 | #[instability::unstable(feature = "reexport")] 283 | pub use private::unstable_private_function as unstable_unstable_export; 284 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides attribute macros for specifying API stability of public API items of a 2 | //! crate. 3 | //! 4 | //! The Rust standard library has a concept of [API stability] and custom attributes for managing 5 | //! that on a per-item basis, but most of these attributes are not available for normal crates to 6 | //! use, with the exception of the [`deprecated`] attribute. This crate seeks to provide similar 7 | //! attributes on stable Rust, though tuned more toward what the needs of normal crate authors. 8 | //! 9 | //! For complete examples of how to use this crate, check out the source code for the 10 | //! [`instability-example`] crate in the repository 11 | //! 12 | //! Currently, only the [`unstable`] attribute is available. Please see the documentation of that 13 | //! macro for an explanation on what it does and how to use it. 14 | //! 15 | //! [API stability]: https://rustc-dev-guide.rust-lang.org/stability.html 16 | //! [`deprecated`]: 17 | //! https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute 18 | //! [`instability-example`]: https://github.com/ratatui-org/instability/tree/main/example 19 | //! [`unstable`]: macro@unstable 20 | 21 | use proc_macro::TokenStream; 22 | use stable::stable_macro; 23 | use unstable::unstable_macro; 24 | 25 | mod item_like; 26 | mod stable; 27 | mod unstable; 28 | 29 | /// Mark an API as unstable. 30 | /// 31 | /// You can apply this attribute to an item in your public API that you would like to expose to 32 | /// users, but are not yet ready for general use. This is useful when you want to let users try out 33 | /// some new functionality for an API you haven't finished testing or designing, or for whatever 34 | /// reason do not want to commit any stability guarantees for. 35 | /// 36 | /// This attribute does the following things to annotated items: 37 | /// 38 | /// - Changes the visibility of the item from `pub` to `pub(crate)` unless a certain crate feature 39 | /// is enabled. This ensures that internal code within the crate can always use the item, but 40 | /// downstream consumers cannot access it unless they opt-in to the unstable API. 41 | /// - Annotated `impl` blocks will instead be removed. 42 | /// - Changes the Visibility of certain child items of the annotated item (such as struct fields) to 43 | /// match the item's visibility. Children that are not public will not be affected. 44 | /// - Appends an "Stability" section to the item's documentation that notes that the item is 45 | /// unstable and indicates the name of the crate feature to enable it. 46 | /// 47 | /// Child items of annotated modules are unchanged, as it might be desirable to be able to re-export 48 | /// them even if the module visibility is restricted. You should apply the attribute to each item 49 | /// within the module with the same feature name if you want to restrict the module's contents 50 | /// itself and not just the module namespace. 51 | /// 52 | /// Note that unlike the [`unstable`][std-unstable] attribute used in the standard library, this 53 | /// attribute does not apply itself recursively to child items. 54 | /// 55 | /// [std-unstable]: https://rustc-dev-guide.rust-lang.org/stability.html 56 | /// 57 | /// Applying this attribute to non-`pub` items is pointless and does nothing. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// The `unstable` attribute supports optional arguments that can be passed to control its behavior. 62 | /// 63 | /// - `feature`: the name of the unstable feature that should control this item's availability. This 64 | /// will have the string `unstable-` prepended to it. If not specified, the item will instead be 65 | /// guarded by a catch-all `unstable` feature. 66 | /// - `issue`: a link or reference to a tracking issue for the unstable feature. This will be 67 | /// included in the item's documentation. 68 | /// 69 | /// # Examples 70 | /// 71 | /// We can apply the attribute to a public function like so: 72 | /// 73 | /// ``` 74 | /// /// This function does something really risky! 75 | /// /// 76 | /// /// Don't use it yet! 77 | /// #[instability::unstable(feature = "risky-function")] 78 | /// pub fn risky_function() { 79 | /// unimplemented!() 80 | /// } 81 | /// ``` 82 | /// 83 | /// This will essentially be expanded to the following: 84 | /// 85 | /// ``` 86 | /// /// This function does something really risky! 87 | /// /// 88 | /// /// Don't use it yet! 89 | /// /// 90 | /// /// # Availability 91 | /// /// 92 | /// /// **This API is marked as unstable** and is only available when the `unstable-risky-function` 93 | /// /// crate feature is enabled. This comes with no stability guarantees, and could be changed or 94 | /// /// removed at any time. 95 | /// #[cfg(feature = "unstable-risky-function")] 96 | /// pub fn risky_function() { 97 | /// unimplemented!() 98 | /// } 99 | /// 100 | /// /// This function does something really risky! 101 | /// /// 102 | /// /// Don't use it yet! 103 | /// #[cfg(not(feature = "unstable-risky-function"))] 104 | /// pub(crate) fn risky_function() { 105 | /// unimplemented!() 106 | /// } 107 | /// ``` 108 | /// 109 | /// We can also apply the attribute to an `impl` block like so: 110 | /// 111 | /// ``` 112 | /// /// This structure is responsible for bar. 113 | /// pub struct Foo; 114 | /// 115 | /// #[instability::unstable(feature = "unstable-dependency")] 116 | /// impl Default for Foo { 117 | /// fn default() -> Self { 118 | /// unimplemented!() 119 | /// } 120 | /// } 121 | /// ``` 122 | #[proc_macro_attribute] 123 | pub fn unstable(args: TokenStream, input: TokenStream) -> TokenStream { 124 | unstable_macro(args.into(), input.into()).into() 125 | } 126 | 127 | /// Mark an API as stable. 128 | /// 129 | /// You can apply this attribute to an item in your public API that you would like to expose to 130 | /// users, and are ready to make a stability guarantee for. This is useful when you have finished 131 | /// testing and designing an API and are ready to commit to its design and stability. 132 | /// 133 | /// This attribute does the following things to annotated items: 134 | /// 135 | /// - Appends a "Stability" section to the item's documentation that notes that the item is stable 136 | /// and indicates the version at which it was stabilized. 137 | /// 138 | /// # Arguments 139 | /// 140 | /// The `stable` attribute supports optional arguments that can be passed to control its behavior. 141 | /// 142 | /// - `since`: the version at which the item was stabilized. This should be a string that follows 143 | /// the [Semantic Versioning](https://semver.org) convention. If not specified, the item will be 144 | /// marked as stable with no version information. 145 | /// - `issue`: a link or reference to a tracking issue for the stabilized feature. This will be 146 | /// included in the item's documentation. 147 | /// 148 | /// # Examples 149 | /// 150 | /// We can apply the attribute to a public function like so: 151 | /// 152 | /// ``` 153 | /// /// This function does something really risky! 154 | /// /// 155 | /// /// Don't use it yet! 156 | /// #[instability::stable(since = "v1.0.0")] 157 | /// pub fn stable_function() { 158 | /// unimplemented!() 159 | /// } 160 | /// ``` 161 | /// 162 | /// This will essentially be expanded to the following: 163 | /// 164 | /// ``` 165 | /// /// This function does something really risky! 166 | /// /// 167 | /// /// Don't use it yet! 168 | /// /// 169 | /// /// # Stability 170 | /// /// 171 | /// /// This API was stabilized in version 1.0.0. 172 | /// pub fn stable_function() { 173 | /// unimplemented!() 174 | /// } 175 | /// ``` 176 | /// 177 | /// Applying this attribute to non-`pub` items is pointless and does nothing. 178 | /// 179 | /// # Panics 180 | /// 181 | /// This macro will panic if applied to an unsupported item type. 182 | /// 183 | /// # Limitations 184 | /// 185 | /// This attribute does not change the visibility of the annotated item. You should ensure that the 186 | /// item's visibility is set to `pub` if you want it to be part of your crate's public API. 187 | /// 188 | /// # See also 189 | /// 190 | /// - The [`unstable`] attribute for marking an API as unstable. 191 | /// 192 | /// [`unstable`]: macro@unstable 193 | #[proc_macro_attribute] 194 | pub fn stable(args: TokenStream, input: TokenStream) -> TokenStream { 195 | stable_macro(args.into(), input.into()).into() 196 | } 197 | -------------------------------------------------------------------------------- /src/stable.rs: -------------------------------------------------------------------------------- 1 | use darling::{ast::NestedMeta, Error, FromMeta}; 2 | use indoc::formatdoc; 3 | use proc_macro2::TokenStream; 4 | use quote::ToTokens; 5 | use syn::{parse_quote, Item}; 6 | 7 | use crate::item_like::{ItemLike, Stability}; 8 | 9 | pub fn stable_macro(args: TokenStream, input: TokenStream) -> TokenStream { 10 | let attributes = match NestedMeta::parse_meta_list(args) { 11 | Ok(attributes) => attributes, 12 | Err(err) => return Error::from(err).write_errors(), 13 | }; 14 | let unstable_attribute = match StableAttribute::from_list(&attributes) { 15 | Ok(attributes) => attributes, 16 | Err(err) => return err.write_errors(), 17 | }; 18 | match syn::parse2::(input) { 19 | Ok(item) => match item { 20 | Item::Type(item_type) => unstable_attribute.expand(item_type), 21 | Item::Enum(item_enum) => unstable_attribute.expand(item_enum), 22 | Item::Struct(item_struct) => unstable_attribute.expand(item_struct), 23 | Item::Fn(item_fn) => unstable_attribute.expand(item_fn), 24 | Item::Mod(item_mod) => unstable_attribute.expand(item_mod), 25 | Item::Trait(item_trait) => unstable_attribute.expand(item_trait), 26 | Item::Const(item_const) => unstable_attribute.expand(item_const), 27 | Item::Static(item_static) => unstable_attribute.expand(item_static), 28 | Item::Use(item_use) => unstable_attribute.expand(item_use), 29 | Item::Impl(item_impl) => unstable_attribute.expand_impl(item_impl), 30 | _ => panic!("unsupported item type"), 31 | }, 32 | Err(err) => Error::from(err).write_errors(), 33 | } 34 | } 35 | 36 | #[derive(Debug, Default, FromMeta)] 37 | pub struct StableAttribute { 38 | /// The version at which the item was stabilized. 39 | since: Option, 40 | 41 | /// A link or reference to a tracking issue for the feature. 42 | issue: Option, 43 | } 44 | 45 | impl StableAttribute { 46 | pub fn expand(&self, item: impl ItemLike + ToTokens + Clone) -> TokenStream { 47 | if !item.is_public() { 48 | // We only care about public items. 49 | return item.into_token_stream(); 50 | } 51 | self.expand_impl(item) 52 | } 53 | 54 | pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream { 55 | let doc = if let Some(ref version) = self.since { 56 | formatdoc! {" 57 | # Stability 58 | 59 | This API was stabilized in version {}.", 60 | version.trim_start_matches('v') 61 | } 62 | } else { 63 | formatdoc! {" 64 | # Stability 65 | 66 | This API is stable."} 67 | }; 68 | item.push_attr(parse_quote! { #[doc = #doc] }); 69 | 70 | if let Some(issue) = &self.issue { 71 | let doc = format!("The tracking issue is: `{}`.", issue); 72 | item.push_attr(parse_quote! { #[doc = #doc] }); 73 | } 74 | item.into_token_stream() 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use pretty_assertions::assert_eq; 81 | use quote::quote; 82 | use syn::parse_quote; 83 | 84 | use super::*; 85 | 86 | #[test] 87 | fn expand_non_public_item() { 88 | let item: syn::ItemStruct = parse_quote! { 89 | struct MyStruct; 90 | }; 91 | let stable = StableAttribute::default(); 92 | let tokens = stable.expand(item.clone()); 93 | assert_eq!(tokens.to_string(), quote! { struct MyStruct; }.to_string()); 94 | } 95 | 96 | const STABLE_DOC: &str = "# Stability\n\nThis API is stable."; 97 | const SINCE_DOC: &str = "# Stability\n\nThis API was stabilized in version 1.0.0."; 98 | const ISSUE_DOC: &str = "The tracking issue is: `#123`."; 99 | 100 | #[test] 101 | fn expand_with_since() { 102 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 103 | let stable = StableAttribute { 104 | since: Some("v1.0.0".to_string()), 105 | issue: None, 106 | }; 107 | let tokens = stable.expand(item); 108 | let expected = quote! { 109 | #[doc = #SINCE_DOC] 110 | pub type Foo = Bar; 111 | }; 112 | assert_eq!(tokens.to_string(), expected.to_string()); 113 | } 114 | 115 | #[test] 116 | fn expand_with_issue() { 117 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 118 | let stable = StableAttribute { 119 | since: None, 120 | issue: Some("#123".to_string()), 121 | }; 122 | let tokens = stable.expand(item); 123 | let expected = quote! { 124 | #[doc = #STABLE_DOC] 125 | #[doc = #ISSUE_DOC] 126 | pub type Foo = Bar; 127 | }; 128 | assert_eq!(tokens.to_string(), expected.to_string()); 129 | } 130 | 131 | #[test] 132 | fn expand_with_since_and_issue() { 133 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 134 | let stable = StableAttribute { 135 | since: Some("v1.0.0".to_string()), 136 | issue: Some("#123".to_string()), 137 | }; 138 | let tokens = stable.expand(item); 139 | let expected = quote! { 140 | #[doc = #SINCE_DOC] 141 | #[doc = #ISSUE_DOC] 142 | pub type Foo = Bar; 143 | }; 144 | assert_eq!(tokens.to_string(), expected.to_string()); 145 | } 146 | 147 | #[test] 148 | fn expand_public_type() { 149 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 150 | let stable = StableAttribute::default(); 151 | let tokens = stable.expand(item); 152 | let expected = quote! { 153 | #[doc = #STABLE_DOC] 154 | pub type Foo = Bar; 155 | }; 156 | assert_eq!(tokens.to_string(), expected.to_string()); 157 | } 158 | 159 | #[test] 160 | fn expand_public_struct() { 161 | let item: syn::ItemStruct = parse_quote! { 162 | pub struct Foo { 163 | pub field: i32, 164 | } 165 | }; 166 | let stable = StableAttribute::default(); 167 | let tokens = stable.expand(item); 168 | let expected = quote! { 169 | #[doc = #STABLE_DOC] 170 | pub struct Foo { 171 | pub field: i32, 172 | } 173 | }; 174 | assert_eq!(tokens.to_string(), expected.to_string()); 175 | } 176 | 177 | #[test] 178 | fn expand_public_enum() { 179 | let item: syn::ItemEnum = parse_quote! { 180 | pub enum Foo { 181 | A, 182 | B, 183 | } 184 | }; 185 | let stable = StableAttribute::default(); 186 | let tokens = stable.expand(item); 187 | let expected = quote! { 188 | #[doc = #STABLE_DOC] 189 | pub enum Foo { 190 | A, 191 | B, 192 | } 193 | }; 194 | assert_eq!(tokens.to_string(), expected.to_string()); 195 | } 196 | 197 | #[test] 198 | fn expand_public_fn() { 199 | let item: syn::ItemFn = parse_quote! { 200 | pub fn foo() {} 201 | }; 202 | let stable = StableAttribute::default(); 203 | let tokens = stable.expand(item); 204 | let expected = quote! { 205 | #[doc = #STABLE_DOC] 206 | pub fn foo() {} 207 | }; 208 | assert_eq!(tokens.to_string(), expected.to_string()); 209 | } 210 | 211 | #[test] 212 | fn expand_public_trait() { 213 | let item: syn::ItemTrait = parse_quote! { 214 | pub trait Foo { 215 | fn bar(&self); 216 | } 217 | }; 218 | let stable = StableAttribute::default(); 219 | let tokens = stable.expand(item); 220 | let expected = quote! { 221 | #[doc = #STABLE_DOC] 222 | pub trait Foo { 223 | fn bar(&self); 224 | } 225 | }; 226 | assert_eq!(tokens.to_string(), expected.to_string()); 227 | } 228 | 229 | #[test] 230 | fn expand_public_const() { 231 | let item: syn::ItemConst = parse_quote! { 232 | pub const FOO: i32 = 42; 233 | }; 234 | let stable = StableAttribute::default(); 235 | let tokens = stable.expand(item); 236 | let expected = quote! { 237 | #[doc = #STABLE_DOC] 238 | pub const FOO: i32 = 42; 239 | }; 240 | assert_eq!(tokens.to_string(), expected.to_string()); 241 | } 242 | 243 | #[test] 244 | fn expand_public_static() { 245 | let item: syn::ItemStatic = parse_quote! { 246 | pub static FOO: i32 = 42; 247 | }; 248 | let stable = StableAttribute::default(); 249 | let tokens = stable.expand(item); 250 | let expected = quote! { 251 | #[doc = #STABLE_DOC] 252 | pub static FOO: i32 = 42; 253 | }; 254 | assert_eq!(tokens.to_string(), expected.to_string()); 255 | } 256 | 257 | #[test] 258 | fn expand_public_mod() { 259 | let item: syn::ItemMod = parse_quote! { 260 | pub mod foo { 261 | pub fn bar() {} 262 | } 263 | }; 264 | let stable = StableAttribute::default(); 265 | let tokens = stable.expand(item); 266 | let expected = quote! { 267 | #[doc = #STABLE_DOC] 268 | pub mod foo { 269 | pub fn bar() {} 270 | } 271 | }; 272 | assert_eq!(tokens.to_string(), expected.to_string()); 273 | } 274 | 275 | #[test] 276 | fn expand_public_use() { 277 | let item: syn::ItemUse = parse_quote! { 278 | pub use crate::foo::bar; 279 | }; 280 | let stable = StableAttribute::default(); 281 | let tokens = stable.expand(item); 282 | let expected = quote! { 283 | #[doc = #STABLE_DOC] 284 | pub use crate::foo::bar; 285 | }; 286 | assert_eq!(tokens.to_string(), expected.to_string()); 287 | } 288 | 289 | #[test] 290 | fn expand_impl_block() { 291 | let item: syn::ItemImpl = parse_quote! { 292 | impl Default for crate::foo::Foo {} 293 | }; 294 | let tokens = StableAttribute::default().expand_impl(item); 295 | let expected = quote! { 296 | #[doc = #STABLE_DOC] 297 | impl Default for crate::foo::Foo {} 298 | }; 299 | assert_eq!(tokens.to_string(), expected.to_string()); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/unstable.rs: -------------------------------------------------------------------------------- 1 | use darling::{ast::NestedMeta, Error, FromMeta}; 2 | use indoc::formatdoc; 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::{parse_quote, Item}; 6 | 7 | use crate::item_like::{ItemLike, Stability}; 8 | 9 | pub fn unstable_macro(args: TokenStream, input: TokenStream) -> TokenStream { 10 | let attributes = match NestedMeta::parse_meta_list(args) { 11 | Ok(attributes) => attributes, 12 | Err(err) => return Error::from(err).write_errors(), 13 | }; 14 | let unstable_attribute = match UnstableAttribute::from_list(&attributes) { 15 | Ok(attributes) => attributes, 16 | Err(err) => return err.write_errors(), 17 | }; 18 | match syn::parse2::(input) { 19 | Ok(item) => match item { 20 | Item::Type(item_type) => unstable_attribute.expand(item_type), 21 | Item::Enum(item_enum) => unstable_attribute.expand(item_enum), 22 | Item::Struct(item_struct) => unstable_attribute.expand(item_struct), 23 | Item::Fn(item_fn) => unstable_attribute.expand(item_fn), 24 | Item::Mod(item_mod) => unstable_attribute.expand(item_mod), 25 | Item::Trait(item_trait) => unstable_attribute.expand(item_trait), 26 | Item::Const(item_const) => unstable_attribute.expand(item_const), 27 | Item::Static(item_static) => unstable_attribute.expand(item_static), 28 | Item::Use(item_use) => unstable_attribute.expand(item_use), 29 | Item::Impl(item_impl) => unstable_attribute.expand_impl(item_impl), 30 | _ => panic!("unsupported item type"), 31 | }, 32 | Err(err) => Error::from(err).write_errors(), 33 | } 34 | } 35 | 36 | #[derive(Debug, Default, FromMeta)] 37 | pub struct UnstableAttribute { 38 | /// The name of the feature that enables the unstable API. 39 | /// 40 | /// If not specified, the item will instead be guarded by a catch-all `unstable` feature. 41 | feature: Option, 42 | 43 | /// A link or reference to a tracking issue for the unstable feature. 44 | /// 45 | /// This will be included in the item's documentation. 46 | issue: Option, 47 | } 48 | 49 | impl UnstableAttribute { 50 | pub fn expand(&self, mut item: impl ItemLike + ToTokens + Clone) -> TokenStream { 51 | if !item.is_public() { 52 | // We only care about public items. 53 | return item.into_token_stream(); 54 | } 55 | 56 | let feature_flag = self.feature_flag(); 57 | self.add_doc(&mut item); 58 | 59 | let mut hidden_item = item.clone(); 60 | hidden_item.set_visibility(parse_quote! { pub(crate) }); 61 | 62 | let allows = item 63 | .allowed_lints() 64 | .into_iter() 65 | .map(|ident| quote! { #[allow(#ident)] }); 66 | 67 | quote! { 68 | #[cfg(any(doc, feature = #feature_flag))] 69 | #[cfg_attr(docsrs, doc(cfg(feature = #feature_flag)))] 70 | #item 71 | 72 | #[cfg(not(any(doc, feature = #feature_flag)))] 73 | #(#allows)* 74 | #hidden_item 75 | } 76 | } 77 | 78 | pub fn expand_impl(&self, mut item: impl Stability + ToTokens) -> TokenStream { 79 | let feature_flag = self.feature_flag(); 80 | self.add_doc(&mut item); 81 | quote! { 82 | #[cfg(any(doc, feature = #feature_flag))] 83 | #[cfg_attr(docsrs, doc(cfg(feature = #feature_flag)))] 84 | #item 85 | } 86 | } 87 | 88 | fn add_doc(&self, item: &mut impl Stability) { 89 | let feature_flag = self.feature_flag(); 90 | let doc = formatdoc! {" 91 | # Stability 92 | 93 | **This API is marked as unstable** and is only available when the `{feature_flag}` 94 | crate feature is enabled. This comes with no stability guarantees, and could be changed 95 | or removed at any time."}; 96 | item.push_attr(parse_quote! { #[doc = #doc] }); 97 | 98 | if let Some(issue) = &self.issue { 99 | let doc = format!("The tracking issue is: `{}`.", issue); 100 | item.push_attr(parse_quote! { #[doc = #doc] }); 101 | } 102 | } 103 | 104 | fn feature_flag(&self) -> String { 105 | self.feature 106 | .as_deref() 107 | .map_or(String::from("unstable"), |name| format!("unstable-{name}")) 108 | } 109 | } 110 | #[cfg(test)] 111 | mod tests { 112 | use pretty_assertions::assert_eq; 113 | use quote::quote; 114 | use syn::parse_quote; 115 | 116 | use super::*; 117 | 118 | #[test] 119 | fn unstable_feature_flag_default() { 120 | let unstable = UnstableAttribute::default(); 121 | assert_eq!(unstable.feature_flag(), "unstable"); 122 | } 123 | 124 | #[test] 125 | fn unstable_feature_flag_with_feature() { 126 | let unstable = UnstableAttribute { 127 | feature: Some("experimental".to_string()), 128 | issue: None, 129 | }; 130 | assert_eq!(unstable.feature_flag(), "unstable-experimental"); 131 | } 132 | 133 | #[test] 134 | fn expand_non_public_item() { 135 | let item: syn::ItemStruct = parse_quote! { 136 | struct MyStruct; 137 | }; 138 | let unstable = UnstableAttribute::default(); 139 | let tokens = unstable.expand(item.clone()); 140 | assert_eq!(tokens.to_string(), quote! { struct MyStruct; }.to_string()); 141 | } 142 | 143 | const DEFAULT_DOC: &str = "# Stability\n\n**This API is marked as unstable** and is only available when the `unstable`\ncrate feature is enabled. This comes with no stability guarantees, and could be changed\nor removed at any time."; 144 | const WITH_FEATURES_DOC: &str = "# Stability\n\n**This API is marked as unstable** and is only available when the `unstable-experimental`\ncrate feature is enabled. This comes with no stability guarantees, and could be changed\nor removed at any time."; 145 | const ISSUE_DOC: &str = "The tracking issue is: `#123`."; 146 | 147 | #[test] 148 | fn expand_with_feature() { 149 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 150 | let unstable = UnstableAttribute { 151 | feature: Some("experimental".to_string()), 152 | issue: None, 153 | }; 154 | let tokens = unstable.expand(item); 155 | let expected = quote! { 156 | #[cfg(any(doc, feature = "unstable-experimental"))] 157 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable-experimental")))] 158 | #[doc = #WITH_FEATURES_DOC] 159 | pub type Foo = Bar; 160 | 161 | #[cfg(not(any(doc, feature = "unstable-experimental")))] 162 | #[allow(dead_code)] 163 | #[doc = #WITH_FEATURES_DOC] 164 | pub(crate) type Foo = Bar; 165 | }; 166 | assert_eq!(tokens.to_string(), expected.to_string()); 167 | } 168 | 169 | #[test] 170 | fn expand_with_issue() { 171 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 172 | let unstable = UnstableAttribute { 173 | feature: None, 174 | issue: Some("#123".to_string()), 175 | }; 176 | let tokens = unstable.expand(item); 177 | let expected = quote! { 178 | #[cfg(any(doc, feature = "unstable"))] 179 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 180 | #[doc = #DEFAULT_DOC] 181 | #[doc = #ISSUE_DOC] 182 | pub type Foo = Bar; 183 | 184 | #[cfg(not(any(doc, feature = "unstable")))] 185 | #[allow(dead_code)] 186 | #[doc = #DEFAULT_DOC] 187 | #[doc = #ISSUE_DOC] 188 | pub(crate) type Foo = Bar; 189 | }; 190 | assert_eq!(tokens.to_string(), expected.to_string()); 191 | } 192 | 193 | #[test] 194 | fn expand_public_type() { 195 | let item: syn::ItemType = parse_quote! { pub type Foo = Bar; }; 196 | let tokens = UnstableAttribute::default().expand(item); 197 | let expected = quote! { 198 | #[cfg(any(doc, feature = "unstable"))] 199 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 200 | #[doc = #DEFAULT_DOC] 201 | pub type Foo = Bar; 202 | 203 | #[cfg(not(any(doc, feature = "unstable")))] 204 | #[allow(dead_code)] 205 | #[doc = #DEFAULT_DOC] 206 | pub(crate) type Foo = Bar; 207 | }; 208 | assert_eq!(tokens.to_string(), expected.to_string()); 209 | } 210 | 211 | #[test] 212 | fn expand_public_struct() { 213 | let item: syn::ItemStruct = parse_quote! { 214 | pub struct Foo { 215 | pub field: i32, 216 | } 217 | }; 218 | let tokens = UnstableAttribute::default().expand(item); 219 | let expected = quote! { 220 | #[cfg(any(doc, feature = "unstable"))] 221 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 222 | #[doc = #DEFAULT_DOC] 223 | pub struct Foo { 224 | pub field: i32, 225 | } 226 | 227 | #[cfg(not(any(doc, feature = "unstable")))] 228 | #[allow(dead_code)] 229 | #[doc = #DEFAULT_DOC] 230 | pub(crate) struct Foo { 231 | pub (crate) field: i32, 232 | } 233 | }; 234 | assert_eq!(tokens.to_string(), expected.to_string()); 235 | } 236 | 237 | #[test] 238 | fn expand_public_enum() { 239 | let item: syn::ItemEnum = parse_quote! { 240 | pub enum Foo { 241 | A, 242 | B, 243 | } 244 | }; 245 | let tokens = UnstableAttribute::default().expand(item); 246 | let expected = quote! { 247 | #[cfg(any(doc, feature = "unstable"))] 248 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 249 | #[doc = #DEFAULT_DOC] 250 | pub enum Foo { 251 | A, 252 | B, 253 | } 254 | 255 | #[cfg(not(any(doc, feature = "unstable")))] 256 | #[allow(dead_code)] 257 | #[doc = #DEFAULT_DOC] 258 | pub(crate) enum Foo { 259 | A, 260 | B, 261 | } 262 | }; 263 | assert_eq!(tokens.to_string(), expected.to_string()); 264 | } 265 | 266 | #[test] 267 | fn expand_public_fn() { 268 | let item: syn::ItemFn = parse_quote! { 269 | pub fn foo() {} 270 | }; 271 | let tokens = UnstableAttribute::default().expand(item); 272 | let expected = quote! { 273 | #[cfg(any(doc, feature = "unstable"))] 274 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 275 | #[doc = #DEFAULT_DOC] 276 | pub fn foo() {} 277 | 278 | #[cfg(not(any(doc, feature = "unstable")))] 279 | #[allow(dead_code)] 280 | #[doc = #DEFAULT_DOC] 281 | pub(crate) fn foo() {} 282 | }; 283 | assert_eq!(tokens.to_string(), expected.to_string()); 284 | } 285 | 286 | #[test] 287 | fn expand_public_trait() { 288 | let item: syn::ItemTrait = parse_quote! { 289 | pub trait Foo { 290 | fn bar(&self); 291 | } 292 | }; 293 | let tokens = UnstableAttribute::default().expand(item); 294 | let expected = quote! { 295 | #[cfg(any(doc, feature = "unstable"))] 296 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 297 | #[doc = #DEFAULT_DOC] 298 | pub trait Foo { 299 | fn bar(&self); 300 | } 301 | 302 | #[cfg(not(any(doc, feature = "unstable")))] 303 | #[allow(dead_code)] 304 | #[doc = #DEFAULT_DOC] 305 | pub(crate) trait Foo { 306 | fn bar(&self); 307 | } 308 | }; 309 | assert_eq!(tokens.to_string(), expected.to_string()); 310 | } 311 | 312 | #[test] 313 | fn expand_public_const() { 314 | let item: syn::ItemConst = parse_quote! { 315 | pub const FOO: i32 = 42; 316 | }; 317 | let tokens = UnstableAttribute::default().expand(item); 318 | let expected = quote! { 319 | #[cfg(any(doc, feature = "unstable"))] 320 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 321 | #[doc = #DEFAULT_DOC] 322 | pub const FOO: i32 = 42; 323 | 324 | #[cfg(not(any(doc, feature = "unstable")))] 325 | #[allow(dead_code)] 326 | #[doc = #DEFAULT_DOC] 327 | pub(crate) const FOO: i32 = 42; 328 | }; 329 | assert_eq!(tokens.to_string(), expected.to_string()); 330 | } 331 | 332 | #[test] 333 | fn expand_public_static() { 334 | let item: syn::ItemStatic = parse_quote! { 335 | pub static FOO: i32 = 42; 336 | }; 337 | let tokens = UnstableAttribute::default().expand(item); 338 | let expected = quote! { 339 | #[cfg(any(doc, feature = "unstable"))] 340 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 341 | #[doc = #DEFAULT_DOC] 342 | pub static FOO: i32 = 42; 343 | 344 | #[cfg(not(any(doc, feature = "unstable")))] 345 | #[allow(dead_code)] 346 | #[doc = #DEFAULT_DOC] 347 | pub(crate) static FOO: i32 = 42; 348 | }; 349 | assert_eq!(tokens.to_string(), expected.to_string()); 350 | } 351 | 352 | #[test] 353 | fn expand_public_mod() { 354 | let item: syn::ItemMod = parse_quote! { 355 | pub mod foo { 356 | pub fn bar() {} 357 | } 358 | }; 359 | let tokens = UnstableAttribute::default().expand(item); 360 | let expected = quote! { 361 | #[cfg(any(doc, feature = "unstable"))] 362 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 363 | #[doc = #DEFAULT_DOC] 364 | pub mod foo { 365 | pub fn bar() {} 366 | } 367 | 368 | #[cfg(not(any(doc, feature = "unstable")))] 369 | #[allow(dead_code)] 370 | #[doc = #DEFAULT_DOC] 371 | pub(crate) mod foo { 372 | pub fn bar() {} 373 | } 374 | }; 375 | assert_eq!(tokens.to_string(), expected.to_string()); 376 | } 377 | 378 | #[test] 379 | fn expand_public_use() { 380 | let item: syn::ItemUse = parse_quote! { 381 | pub use crate::foo::bar; 382 | }; 383 | let tokens = UnstableAttribute::default().expand(item); 384 | let expected = quote! { 385 | #[cfg(any(doc, feature = "unstable"))] 386 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 387 | #[doc = #DEFAULT_DOC] 388 | pub use crate::foo::bar; 389 | 390 | #[cfg(not(any(doc, feature = "unstable")))] 391 | #[allow(unused_imports)] 392 | #[doc = #DEFAULT_DOC] 393 | pub(crate) use crate::foo::bar; 394 | }; 395 | assert_eq!(tokens.to_string(), expected.to_string()); 396 | } 397 | 398 | #[test] 399 | fn expand_impl_block() { 400 | let item: syn::ItemImpl = parse_quote! { 401 | impl Default for crate::foo::Foo {} 402 | }; 403 | let tokens = UnstableAttribute::default().expand_impl(item); 404 | let expected = quote! { 405 | #[cfg(any(doc, feature = "unstable"))] 406 | #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] 407 | #[doc = #DEFAULT_DOC] 408 | impl Default for crate::foo::Foo {} 409 | }; 410 | assert_eq!(tokens.to_string(), expected.to_string()); 411 | } 412 | } 413 | --------------------------------------------------------------------------------