├── .github ├── .cspell │ ├── project-dictionary.txt │ └── rust-dependencies.txt ├── workflows │ ├── release.yml │ └── ci.yml └── dependabot.yml ├── tools ├── .tidy-check-license-headers ├── publish.sh └── tidy.sh ├── tests └── basic │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ └── src │ └── lib.rs ├── .gitattributes ├── .taplo.toml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .clippy.toml ├── .editorconfig ├── .deny.toml ├── LICENSE-MIT ├── .shellcheckrc ├── .cspell.json ├── .rustfmt.toml ├── src ├── error.rs ├── quote.rs └── lib.rs ├── README.md ├── CHANGELOG.md ├── Cargo.toml └── LICENSE-APACHE /.github/.cspell/project-dictionary.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/.tidy-check-license-headers: -------------------------------------------------------------------------------- 1 | git ls-files 2 | -------------------------------------------------------------------------------- /tests/basic/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target-dir = "../../target" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | .github/.cspell/rust-dependencies.txt linguist-generated 3 | -------------------------------------------------------------------------------- /.github/.cspell/rust-dependencies.txt: -------------------------------------------------------------------------------- 1 | // This file is @generated by tidy.sh. 2 | // It is not intended for manual editing. 3 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | # Taplo configuration 2 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 3 | 4 | [formatting] 5 | align_comments = false 6 | allowed_blank_lines = 1 7 | array_auto_collapse = false 8 | array_auto_expand = false 9 | indent_string = " " 10 | -------------------------------------------------------------------------------- /tests/basic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic" 3 | edition = "2018" 4 | 5 | [dependencies] 6 | coverage-helper = { path = "../.." } 7 | 8 | [workspace] 9 | 10 | [lints.rust] 11 | unexpected_cfgs = { level = "warn", check-cfg = [ 12 | 'cfg(coverage_nightly)', 13 | ] } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .venv 4 | 5 | # For platform and editor specific settings, it is recommended to add to 6 | # a global .gitignore file. 7 | # Refs: https://docs.github.com/en/github/using-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer 8 | -------------------------------------------------------------------------------- /.markdownlint-cli2.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/DavidAnson/markdownlint#rules--aliases 2 | config: 3 | line-length: false 4 | no-duplicate-heading: false 5 | no-inline-html: false 6 | no-emphasis-as-heading: false 7 | 8 | # https://github.com/DavidAnson/markdownlint-cli2#markdownlint-cli2jsonc 9 | noBanner: true 10 | noProgress: true 11 | -------------------------------------------------------------------------------- /tests/basic/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 4 | #![allow(dead_code, deprecated)] 5 | 6 | #[coverage_helper::test] 7 | fn a() { 8 | if true { 9 | b(); 10 | } else { 11 | b(); 12 | } 13 | } 14 | 15 | fn b() { 16 | println!(); 17 | } 18 | -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | # Clippy configuration 2 | # https://doc.rust-lang.org/nightly/clippy/lint_configuration.html 3 | 4 | allow-private-module-inception = true 5 | avoid-breaking-exported-api = false 6 | disallowed-names = [] 7 | disallowed-macros = [ 8 | { path = "std::dbg", reason = "it is okay to use during development, but please do not include it in main branch" }, 9 | ] 10 | disallowed-methods = [ 11 | ] 12 | disallowed-types = [ 13 | ] 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig configuration 2 | # https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.{json,md,rb,yml,yaml}] 15 | indent_size = 2 16 | 17 | [*.{js,yml,yaml}] 18 | quote_type = single 19 | 20 | [*.sh] 21 | binary_next_line = true 22 | switch_case_indent = true 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | defaults: 12 | run: 13 | shell: bash --noprofile --norc -CeEuxo pipefail {0} 14 | 15 | jobs: 16 | create-release: 17 | if: github.repository_owner == 'taiki-e' 18 | uses: taiki-e/github-actions/.github/workflows/create-release.yml@main 19 | permissions: 20 | contents: write 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: '' 9 | labels: [] 10 | groups: 11 | cargo: 12 | patterns: 13 | - '*' 14 | - package-ecosystem: github-actions 15 | directory: / 16 | schedule: 17 | interval: daily 18 | commit-message: 19 | prefix: '' 20 | labels: [] 21 | groups: 22 | github-actions: 23 | patterns: 24 | - '*' 25 | -------------------------------------------------------------------------------- /.deny.toml: -------------------------------------------------------------------------------- 1 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 2 | [advisories] 3 | yanked = "deny" 4 | git-fetch-with-cli = true 5 | ignore = [ 6 | ] 7 | 8 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 9 | [bans] 10 | multiple-versions = "warn" 11 | wildcards = "deny" 12 | allow-wildcard-paths = true 13 | build.executables = "deny" 14 | build.interpreted = "deny" 15 | build.include-dependencies = true 16 | build.include-workspace = false # covered by tools/tidy.sh 17 | build.include-archives = true 18 | build.allow-build-scripts = [ 19 | { name = "coverage-helper" }, 20 | ] 21 | build.bypass = [ 22 | ] 23 | 24 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 25 | [licenses] 26 | unused-allowed-license = "deny" 27 | private.ignore = true 28 | allow = [ 29 | "Apache-2.0", 30 | "MIT", 31 | ] 32 | 33 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 34 | [sources] 35 | unknown-registry = "deny" 36 | unknown-git = "deny" 37 | allow-git = [ 38 | ] 39 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | # ShellCheck configuration 2 | # https://github.com/koalaman/shellcheck/blob/HEAD/shellcheck.1.md#rc-files 3 | 4 | # See also: 5 | # https://github.com/koalaman/shellcheck/wiki/Optional 6 | # https://google.github.io/styleguide/shellguide.html 7 | 8 | # https://github.com/koalaman/shellcheck/wiki/SC2249 9 | # enable=add-default-case 10 | 11 | # https://github.com/koalaman/shellcheck/wiki/SC2244 12 | enable=avoid-nullary-conditions 13 | 14 | # https://github.com/koalaman/shellcheck/wiki/SC2312 15 | # enable=check-extra-masked-returns 16 | 17 | # https://github.com/koalaman/shellcheck/wiki/SC2310 18 | # https://github.com/koalaman/shellcheck/wiki/SC2311 19 | # enable=check-set-e-suppressed 20 | 21 | # enable=check-unassigned-uppercase 22 | 23 | # https://github.com/koalaman/shellcheck/wiki/SC2230 24 | enable=deprecate-which 25 | 26 | # https://github.com/koalaman/shellcheck/wiki/SC2248 27 | enable=quote-safe-variables 28 | 29 | # https://github.com/koalaman/shellcheck/wiki/SC2292 30 | # https://google.github.io/styleguide/shellguide.html#s6.3-tests 31 | enable=require-double-brackets 32 | 33 | # https://github.com/koalaman/shellcheck/wiki/SC2250 34 | # https://google.github.io/styleguide/shellguide.html#s5.6-variable-expansion 35 | enable=require-variable-braces 36 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "gitignoreRoot": ".", 4 | "useGitignore": true, 5 | "dictionaryDefinitions": [ 6 | { 7 | "name": "organization-dictionary", 8 | "path": "https://raw.githubusercontent.com/taiki-e/github-actions/HEAD/.github/.cspell/organization-dictionary.txt", 9 | "addWords": true 10 | }, 11 | { 12 | "name": "project-dictionary", 13 | "path": "./.github/.cspell/project-dictionary.txt", 14 | "addWords": true 15 | }, 16 | { 17 | "name": "rust-dependencies", 18 | "path": "./.github/.cspell/rust-dependencies.txt", 19 | "addWords": true 20 | } 21 | ], 22 | "dictionaries": [ 23 | "organization-dictionary", 24 | "project-dictionary", 25 | "rust-dependencies" 26 | ], 27 | "ignoreRegExpList": [ 28 | // Copyright notice 29 | "Copyright .*", 30 | // GHA actions/workflows 31 | "uses: .+@", 32 | // GHA context (repo name, owner name, etc.) 33 | "github.\\w+ (=|!)= '.+'", 34 | // GH username 35 | "( |\\[)@[\\w_-]+", 36 | // Git config username 37 | "git config user.name .*", 38 | // Username in todo comment 39 | "(TODO|FIXME)\\([\\w_., -]+\\)", 40 | // Cargo.toml authors 41 | "authors *= *\\[.*\\]", 42 | "\".* <[\\w_.+-]+@[\\w.-]+>\"" 43 | ], 44 | "languageSettings": [ 45 | { 46 | "languageId": ["*"], 47 | "dictionaries": ["bash", "rust"] 48 | } 49 | ], 50 | "ignorePaths": [] 51 | } 52 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Rustfmt configuration 2 | # https://github.com/rust-lang/rustfmt/blob/HEAD/Configurations.md 3 | 4 | # Rustfmt cannot format long lines inside macros, but this option detects this. 5 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3391) 6 | error_on_line_overflow = true 7 | 8 | # Override the default formatting style. 9 | # See https://internals.rust-lang.org/t/running-rustfmt-on-rust-lang-rust-and-other-rust-lang-repositories/8732/81. 10 | use_small_heuristics = "Max" 11 | # This is the default of 2024 edition https://github.com/rust-lang/rust/pull/114764. 12 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3370) 13 | overflow_delimited_expr = true 14 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/4991). 15 | imports_granularity = "Crate" 16 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/5083). 17 | group_imports = "StdExternalCrate" 18 | 19 | # Apply rustfmt to more places. 20 | # This is unstable (tracking issue: https://github.com/rust-lang/rustfmt/issues/3348). 21 | format_code_in_doc_comments = true 22 | 23 | # Automatically fix deprecated style. 24 | use_field_init_shorthand = true 25 | use_try_shorthand = true 26 | 27 | # Set the default settings again to always apply the proper formatting without 28 | # being affected by the editor settings. 29 | edition = "2018" 30 | hard_tabs = false 31 | newline_style = "Unix" 32 | tab_spaces = 4 33 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | use std::iter::FromIterator; 4 | 5 | use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; 6 | 7 | macro_rules! format_err { 8 | ($span:expr, $msg:expr $(,)*) => { 9 | crate::error::Error::new($span, String::from($msg)) 10 | }; 11 | ($span:expr, $($tt:tt)*) => { 12 | format_err!($span, format!($($tt)*)) 13 | }; 14 | } 15 | 16 | pub(crate) struct Error { 17 | span: Span, 18 | msg: String, 19 | } 20 | 21 | impl Error { 22 | pub(crate) fn new(span: Span, msg: String) -> Self { 23 | Self { span, msg } 24 | } 25 | 26 | // https://github.com/dtolnay/syn/blob/1.0.39/src/error.rs#L218-L237 27 | pub(crate) fn into_compile_error(self) -> TokenStream { 28 | // compile_error!($msg) 29 | TokenStream::from_iter(vec![ 30 | TokenTree::Ident(Ident::new("compile_error", self.span)), 31 | TokenTree::Punct({ 32 | let mut punct = Punct::new('!', Spacing::Alone); 33 | punct.set_span(self.span); 34 | punct 35 | }), 36 | TokenTree::Group({ 37 | let mut group = Group::new(Delimiter::Brace, { 38 | TokenStream::from_iter(vec![TokenTree::Literal({ 39 | let mut string = Literal::string(&self.msg); 40 | string.set_span(self.span); 41 | string 42 | })]) 43 | }); 44 | group.set_span(self.span); 45 | group 46 | }), 47 | ]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/quote.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | // Based on https://github.com/dtolnay/proc-macro-hack/blob/0.5.19/src/quote.rs. 4 | 5 | macro_rules! quote { 6 | ($($tt:tt)*) => {{ 7 | let mut tokens = ::proc_macro::TokenStream::new(); 8 | quote_each_token!(tokens $($tt)*); 9 | tokens 10 | }}; 11 | } 12 | 13 | macro_rules! quote_each_token { 14 | ($tokens:ident $ident:ident $($rest:tt)*) => { 15 | <::proc_macro::TokenStream as ::std::iter::Extend<_>>::extend( 16 | &mut $tokens, 17 | ::std::iter::once( 18 | ::proc_macro::TokenTree::Ident( 19 | ::proc_macro::Ident::new( 20 | stringify!($ident), 21 | ::proc_macro::Span::call_site(), 22 | ), 23 | ), 24 | ), 25 | ); 26 | quote_each_token!($tokens $($rest)*); 27 | }; 28 | ($tokens:ident ( $($inner:tt)* ) $($rest:tt)*) => { 29 | <::proc_macro::TokenStream as ::std::iter::Extend<_>>::extend( 30 | &mut $tokens, 31 | ::std::iter::once( 32 | ::proc_macro::TokenTree::Group( 33 | ::proc_macro::Group::new( 34 | ::proc_macro::Delimiter::Parenthesis, 35 | quote!($($inner)*), 36 | ), 37 | ), 38 | ), 39 | ); 40 | quote_each_token!($tokens $($rest)*); 41 | }; 42 | ($tokens:ident [ $($inner:tt)* ] $($rest:tt)*) => { 43 | <::proc_macro::TokenStream as ::std::iter::Extend<_>>::extend( 44 | &mut $tokens, 45 | ::std::iter::once( 46 | ::proc_macro::TokenTree::Group( 47 | ::proc_macro::Group::new( 48 | ::proc_macro::Delimiter::Bracket, 49 | quote!($($inner)*), 50 | ), 51 | ), 52 | ), 53 | ); 54 | quote_each_token!($tokens $($rest)*); 55 | }; 56 | ($tokens:ident $punct:tt $($rest:tt)*) => { 57 | <::proc_macro::TokenStream as ::std::iter::Extend<_>>::extend( 58 | &mut $tokens, 59 | stringify!($punct).parse::<::proc_macro::TokenStream>(), 60 | ); 61 | quote_each_token!($tokens $($rest)*); 62 | }; 63 | ($tokens:ident) => {}; 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coverage-helper 2 | 3 | [![crates.io](https://img.shields.io/crates/v/coverage-helper?style=flat-square&logo=rust)](https://crates.io/crates/coverage-helper) 4 | [![docs.rs](https://img.shields.io/badge/docs.rs-coverage--helper-blue?style=flat-square&logo=docs.rs)](https://docs.rs/coverage-helper) 5 | [![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)](#license) 6 | [![msrv](https://img.shields.io/badge/msrv-1.38-blue?style=flat-square&logo=rust)](https://www.rust-lang.org) 7 | [![github actions](https://img.shields.io/github/actions/workflow/status/taiki-e/coverage-helper/ci.yml?branch=main&style=flat-square&logo=github)](https://github.com/taiki-e/coverage-helper/actions) 8 | ![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red?style=flat-square) 9 | 10 | 11 | 12 | **Note: This crate is now deprecated in favor of the pattern that is [recommended in the cargo-llvm-cov documentation](https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#exclude-code-from-coverage).** 13 | 14 | > If you want to ignore all `#[test]`-related code, you can use module-level `#[coverage(off)]` attribute: 15 | > 16 | > ```rust 17 | > #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 18 | > 19 | > #[cfg(test)] 20 | > #[cfg_attr(coverage_nightly, coverage(off))] 21 | > mod tests { 22 | > // ... 23 | > } 24 | > ``` 25 | > 26 | > cargo-llvm-cov excludes code contained in the directory named `tests` from the report by default, so you can also use it instead of `#[coverage(off)]` attribute. 27 | 28 | --- 29 | 30 | Helper for . 31 | 32 | **Note:** coverage-helper 0.2 supports `#[coverage(off)]`. 33 | See coverage-helper 0.1 for versions that support `#[no_coverage]`. 34 | 35 | ## Usage 36 | 37 | Add this to your `Cargo.toml`: 38 | 39 | ```toml 40 | [dev-dependencies] 41 | coverage-helper = "0.2" 42 | ``` 43 | 44 | ## Examples 45 | 46 | ```rust 47 | use coverage_helper::test; 48 | 49 | #[test] 50 | fn my_test() { 51 | // ... 52 | } 53 | ``` 54 | 55 | Expanded to: 56 | 57 | ```rust 58 | #[cfg_attr(all(coverage_nightly, test), coverage(off))] 59 | #[::core::prelude::v1::test] 60 | fn my_test() { 61 | // ... 62 | } 63 | ``` 64 | 65 | 66 | 67 | ## License 68 | 69 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or 70 | [MIT license](LICENSE-MIT) at your option. 71 | 72 | Unless you explicitly state otherwise, any contribution intentionally submitted 73 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall 74 | be dual licensed as above, without any additional terms or conditions. 75 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - dev 12 | schedule: 13 | - cron: '0 2 * * *' 14 | workflow_dispatch: 15 | 16 | env: 17 | CARGO_INCREMENTAL: 0 18 | CARGO_NET_GIT_FETCH_WITH_CLI: true 19 | CARGO_NET_RETRY: 10 20 | CARGO_TERM_COLOR: always 21 | RUST_BACKTRACE: 1 22 | RUSTDOCFLAGS: -D warnings 23 | RUSTFLAGS: -D warnings 24 | RUSTUP_MAX_RETRIES: 10 25 | 26 | defaults: 27 | run: 28 | shell: bash --noprofile --norc -CeEuxo pipefail {0} 29 | 30 | concurrency: 31 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 32 | cancel-in-progress: true 33 | 34 | jobs: 35 | check-external-types: 36 | uses: taiki-e/github-actions/.github/workflows/check-external-types.yml@main 37 | deny: 38 | uses: taiki-e/github-actions/.github/workflows/deny.yml@main 39 | docs: 40 | uses: taiki-e/github-actions/.github/workflows/docs.yml@main 41 | miri: 42 | uses: taiki-e/github-actions/.github/workflows/miri.yml@main 43 | msrv: 44 | uses: taiki-e/github-actions/.github/workflows/msrv.yml@main 45 | with: 46 | event_name: ${{ github.event_name }} 47 | tidy: 48 | uses: taiki-e/github-actions/.github/workflows/tidy.yml@main 49 | permissions: 50 | contents: read 51 | pull-requests: write # for gh pr edit --add-assignee 52 | repository-projects: read # for gh pr edit --add-assignee 53 | secrets: inherit 54 | 55 | test: 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | rust: 60 | - stable 61 | - beta 62 | - nightly 63 | runs-on: ubuntu-latest 64 | timeout-minutes: 60 65 | steps: 66 | - uses: taiki-e/checkout-action@v1 67 | - uses: taiki-e/github-actions/install-rust@main 68 | with: 69 | toolchain: ${{ matrix.rust }} 70 | - uses: taiki-e/install-action@cargo-hack 71 | - uses: taiki-e/install-action@cargo-minimal-versions 72 | - uses: taiki-e/install-action@cargo-llvm-cov 73 | - run: cargo test --workspace --all-features 74 | - run: cargo hack build --workspace --no-private --feature-powerset --no-dev-deps 75 | - run: cargo minimal-versions build --workspace --no-private --detach-path-deps=skip-exact --all-features 76 | - run: cargo minimal-versions build --workspace --no-private --detach-path-deps=skip-exact --all-features --direct 77 | - run: cargo llvm-cov --text --fail-under-lines 80 78 | working-directory: tests/basic 79 | - run: cargo llvm-cov --text --fail-under-lines 100 80 | working-directory: tests/basic 81 | if: startsWith(matrix.rust, 'nightly') 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org). 6 | 7 | Releases may yanked if there is a security bug, a soundness bug, or a regression. 8 | 9 | 12 | 13 | ## [Unreleased] 14 | 15 | ## [0.2.4] - 2025-01-18 16 | 17 | - Update documentation to reflect the revert of `coverage_attribute` stabilization. 18 | 19 | ## [0.2.3] - 2024-12-18 20 | 21 | **Note: This crate is now deprecated in favor of the pattern that is [recommended in the cargo-llvm-cov documentation](https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#exclude-code-from-coverage).** 22 | 23 | > If you want to ignore all `#[test]`-related code, you can use module-level `#[coverage(off)]` attribute: 24 | > 25 | > ```rust 26 | > #[cfg(test)] 27 | > #[cfg_attr(coverage, coverage(off))] 28 | > mod tests { 29 | > // ... 30 | > } 31 | > ``` 32 | > 33 | > cargo-llvm-cov excludes code contained in the directory named `tests` from the report by default, so you can also use it instead of `#[coverage(off)]` attribute. 34 | 35 | `#[coverage(off)]` attribute has been stabilized in [rust-lang/rust#130766](https://github.com/rust-lang/rust/pull/130766) (will be included in Rust 1.85). 36 | 37 | ## [0.2.2] - 2024-04-21 38 | 39 | - Update documentation to recommend using `#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]` instead of `#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`. 40 | 41 | ## [0.2.1] - 2024-04-21 42 | 43 | - Respect `RUSTC_WORKSPACE_WRAPPER` when checking availability of `#[coverage(off)]`. 44 | 45 | ## [0.2.0] - 2023-09-14 46 | 47 | **Note:** coverage-helper 0.2 supports `#[coverage(off)]`. 48 | See coverage-helper 0.1 for versions that support `#[no_coverage]`. 49 | 50 | - Use [`#[coverage(off)]`](https://github.com/rust-lang/rust/pull/114656) instead of `#[no_coverage]`. ([#4](https://github.com/taiki-e/coverage-helper/pull/4)) 51 | 52 | ## [0.1.1] - 2023-09-13 53 | 54 | - Prepare for [renaming of `#[no_coverage]` in future nightly](https://github.com/rust-lang/rust/pull/114656). ([#3](https://github.com/taiki-e/coverage-helper/pull/3)) 55 | 56 | ## [0.1.0] - 2022-05-29 57 | 58 | Initial release 59 | 60 | [Unreleased]: https://github.com/taiki-e/coverage-helper/compare/v0.2.4...HEAD 61 | [0.2.4]: https://github.com/taiki-e/coverage-helper/compare/v0.2.3...v0.2.4 62 | [0.2.3]: https://github.com/taiki-e/coverage-helper/compare/v0.2.2...v0.2.3 63 | [0.2.2]: https://github.com/taiki-e/coverage-helper/compare/v0.2.1...v0.2.2 64 | [0.2.1]: https://github.com/taiki-e/coverage-helper/compare/v0.2.0...v0.2.1 65 | [0.2.0]: https://github.com/taiki-e/coverage-helper/compare/v0.1.1...v0.2.0 66 | [0.1.1]: https://github.com/taiki-e/coverage-helper/compare/v0.1.0...v0.1.1 67 | [0.1.0]: https://github.com/taiki-e/coverage-helper/releases/tag/v0.1.0 68 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 OR MIT 2 | 3 | /*! 4 | 5 | 6 | **Note: This crate is now deprecated in favor of the pattern that is [recommended in the cargo-llvm-cov documentation](https://github.com/taiki-e/cargo-llvm-cov?tab=readme-ov-file#exclude-code-from-coverage).** 7 | 8 | > If you want to ignore all `#[test]`-related code, you can use module-level `#[coverage(off)]` attribute: 9 | > 10 | > ```rust 11 | > #![cfg_attr(coverage_nightly, feature(coverage_attribute))] 12 | > 13 | > #[cfg(test)] 14 | > #[cfg_attr(coverage_nightly, coverage(off))] 15 | > mod tests { 16 | > // ... 17 | > } 18 | > ``` 19 | > 20 | > cargo-llvm-cov excludes code contained in the directory named `tests` from the report by default, so you can also use it instead of `#[coverage(off)]` attribute. 21 | 22 | --- 23 | 24 | Helper for . 25 | 26 | **Note:** coverage-helper 0.2 supports `#[coverage(off)]`. 27 | See coverage-helper 0.1 for versions that support `#[no_coverage]`. 28 | 29 | ## Usage 30 | 31 | Add this to your `Cargo.toml`: 32 | 33 | ```toml 34 | [dev-dependencies] 35 | coverage-helper = "0.2" 36 | ``` 37 | 38 | ## Examples 39 | 40 | ```rust 41 | use coverage_helper::test; 42 | 43 | #[test] 44 | fn my_test() { 45 | // ... 46 | } 47 | ``` 48 | 49 | Expanded to: 50 | 51 | ```rust 52 | #[cfg_attr(all(coverage_nightly, test), coverage(off))] 53 | #[::core::prelude::v1::test] 54 | fn my_test() { 55 | // ... 56 | } 57 | ``` 58 | 59 | 60 | */ 61 | 62 | #![doc(test( 63 | no_crate_inject, 64 | attr( 65 | deny(warnings, rust_2018_idioms, single_use_lifetimes), 66 | allow(dead_code, unused_variables, deprecated) 67 | ) 68 | ))] 69 | #![forbid(unsafe_code)] 70 | #![allow(deprecated, clippy::test_attr_in_doctest)] 71 | 72 | // older compilers require explicit `extern crate`. 73 | #[allow(unused_extern_crates)] 74 | extern crate proc_macro; 75 | 76 | #[macro_use] 77 | mod error; 78 | 79 | #[macro_use] 80 | mod quote; 81 | 82 | use proc_macro::{Span, TokenStream}; 83 | 84 | #[deprecated( 85 | since = "0.2.3", 86 | note = "this crate is deprecated in favor of module-level #[coverage(off)] attribute; \ 87 | see \ 88 | for more" 89 | )] 90 | #[proc_macro_attribute] 91 | pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { 92 | if !args.is_empty() { 93 | return format_err!(Span::call_site(), "attribute must be of the form `#[test]`") 94 | .into_compile_error(); 95 | } 96 | let mut out = TokenStream::new(); 97 | if cfg!(coverage_helper_has_coverage_attribute) { 98 | out.extend(quote! { #[cfg_attr(all(coverage_nightly, test), coverage(off))] }); 99 | } 100 | out.extend(quote! { #[::core::prelude::v1::test] }); 101 | out.extend(input); 102 | out 103 | } 104 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coverage-helper" 3 | version = "0.2.4" #publish:version 4 | edition = "2018" 5 | rust-version = "1.38" 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/taiki-e/coverage-helper" 8 | keywords = ["macros", "attribute", "coverage", "testing"] 9 | categories = ["development-tools::testing"] 10 | exclude = ["/.*", "/tools"] 11 | description = """ 12 | Helper for . (deprecated) 13 | """ 14 | 15 | [package.metadata.docs.rs] 16 | targets = ["x86_64-unknown-linux-gnu"] 17 | 18 | [badges] 19 | maintenance = { status = "deprecated" } 20 | 21 | [lib] 22 | proc-macro = true 23 | 24 | [dev-dependencies] 25 | 26 | [lints] 27 | workspace = true 28 | 29 | [workspace] 30 | 31 | # This table is shared by projects under github.com/taiki-e. 32 | # It is not intended for manual editing. 33 | [workspace.lints.rust] 34 | deprecated_safe = "warn" 35 | improper_ctypes = "warn" 36 | improper_ctypes_definitions = "warn" 37 | non_ascii_idents = "warn" 38 | rust_2018_idioms = "warn" 39 | single_use_lifetimes = "warn" 40 | unexpected_cfgs = { level = "warn", check-cfg = [ 41 | 'cfg(coverage_nightly)', 42 | ] } 43 | unreachable_pub = "warn" 44 | # unsafe_op_in_unsafe_fn = "warn" # Set at crate-level instead since https://github.com/rust-lang/rust/pull/100081 is not available on MSRV 45 | [workspace.lints.clippy] 46 | all = "warn" # Downgrade deny-by-default lints 47 | pedantic = "warn" 48 | as_ptr_cast_mut = "warn" 49 | as_underscore = "warn" 50 | default_union_representation = "warn" 51 | inline_asm_x86_att_syntax = "warn" 52 | trailing_empty_array = "warn" 53 | transmute_undefined_repr = "warn" 54 | undocumented_unsafe_blocks = "warn" 55 | # Suppress buggy or noisy clippy lints 56 | bool_assert_comparison = { level = "allow", priority = 1 } 57 | borrow_as_ptr = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/8286 58 | cast_lossless = { level = "allow", priority = 1 } # https://godbolt.org/z/Pv6vbGG6E 59 | declare_interior_mutable_const = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/7665 60 | doc_markdown = { level = "allow", priority = 1 } 61 | float_cmp = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/7725 62 | incompatible_msrv = { level = "allow", priority = 1 } # buggy: doesn't consider cfg, https://github.com/rust-lang/rust-clippy/issues/12280, https://github.com/rust-lang/rust-clippy/issues/12257#issuecomment-2093667187 63 | lint_groups_priority = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/12920 64 | manual_assert = { level = "allow", priority = 1 } 65 | manual_range_contains = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/6455#issuecomment-1225966395 66 | missing_errors_doc = { level = "allow", priority = 1 } 67 | module_name_repetitions = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+module_name_repetitions 68 | naive_bytecount = { level = "allow", priority = 1 } 69 | nonminimal_bool = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+nonminimal_bool 70 | range_plus_one = { level = "allow", priority = 1 } # buggy: https://github.com/rust-lang/rust-clippy/issues?q=is%3Aissue+is%3Aopen+range_plus_one 71 | similar_names = { level = "allow", priority = 1 } 72 | single_match = { level = "allow", priority = 1 } 73 | single_match_else = { level = "allow", priority = 1 } 74 | struct_excessive_bools = { level = "allow", priority = 1 } 75 | struct_field_names = { level = "allow", priority = 1 } 76 | too_many_arguments = { level = "allow", priority = 1 } 77 | too_many_lines = { level = "allow", priority = 1 } 78 | type_complexity = { level = "allow", priority = 1 } 79 | unreadable_literal = { level = "allow", priority = 1 } 80 | -------------------------------------------------------------------------------- /tools/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # SPDX-License-Identifier: Apache-2.0 OR MIT 3 | set -CeEuo pipefail 4 | IFS=$'\n\t' 5 | trap -- 's=$?; printf >&2 "%s\n" "${0##*/}:${LINENO}: \`${BASH_COMMAND}\` exit with ${s}"; exit ${s}' ERR 6 | cd -- "$(dirname -- "$0")"/.. 7 | 8 | # Publish a new release. 9 | # 10 | # USAGE: 11 | # ./tools/publish.sh 12 | # 13 | # Note: This script requires the following tools: 14 | # - parse-changelog 15 | 16 | retry() { 17 | for i in {1..10}; do 18 | if "$@"; then 19 | return 0 20 | else 21 | sleep "${i}" 22 | fi 23 | done 24 | "$@" 25 | } 26 | bail() { 27 | printf >&2 'error: %s\n' "$*" 28 | exit 1 29 | } 30 | 31 | version="${1:?}" 32 | version="${version#v}" 33 | tag_prefix="v" 34 | tag="${tag_prefix}${version}" 35 | changelog="CHANGELOG.md" 36 | if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z\.-]+)?(\+[0-9A-Za-z\.-]+)?$ ]]; then 37 | bail "invalid version format '${version}'" 38 | fi 39 | if [[ $# -gt 1 ]]; then 40 | bail "invalid argument '$2'" 41 | fi 42 | if { sed --help 2>&1 || true; } | grep -Eq -e '-i extension'; then 43 | in_place=(-i '') 44 | else 45 | in_place=(-i) 46 | fi 47 | 48 | # Make sure there is no uncommitted change. 49 | git diff --exit-code 50 | git diff --exit-code --staged 51 | 52 | # Make sure the same release has not been created in the past. 53 | if gh release view "${tag}" &>/dev/null; then 54 | bail "tag '${tag}' has already been created and pushed" 55 | fi 56 | 57 | # Make sure that the release was created from an allowed branch. 58 | if ! git branch | grep -Eq '\* main$'; then 59 | bail "current branch is not 'main'" 60 | fi 61 | if ! git remote -v | grep -F origin | grep -Eq 'github\.com[:/]taiki-e/'; then 62 | bail "cannot publish a new release from fork repository" 63 | fi 64 | 65 | release_date=$(date -u '+%Y-%m-%d') 66 | tags=$(git --no-pager tag | { grep -E "^${tag_prefix}[0-9]+" || true; }) 67 | if [[ -n "${tags}" ]]; then 68 | # Make sure the same release does not exist in changelog. 69 | if grep -Eq "^## \\[${version//./\\.}\\]" "${changelog}"; then 70 | bail "release ${version} already exist in ${changelog}" 71 | fi 72 | if grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 73 | bail "link to ${version} already exist in ${changelog}" 74 | fi 75 | # Update changelog. 76 | remote_url=$(grep -E '^\[Unreleased\]: https://' "${changelog}" | sed -E 's/^\[Unreleased\]: //; s/\.\.\.HEAD$//') 77 | prev_tag="${remote_url#*/compare/}" 78 | remote_url="${remote_url%/compare/*}" 79 | sed -E "${in_place[@]}" \ 80 | -e "s/^## \\[Unreleased\\]/## [Unreleased]\\n\\n## [${version}] - ${release_date}/" \ 81 | -e "s#^\[Unreleased\]: https://.*#[Unreleased]: ${remote_url}/compare/${tag}...HEAD\\n[${version}]: ${remote_url}/compare/${prev_tag}...${tag}#" "${changelog}" 82 | if ! grep -Eq "^## \\[${version//./\\.}\\] - ${release_date}$" "${changelog}"; then 83 | bail "failed to update ${changelog}" 84 | fi 85 | if ! grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 86 | bail "failed to update ${changelog}" 87 | fi 88 | else 89 | # Make sure the release exists in changelog. 90 | if ! grep -Eq "^## \\[${version//./\\.}\\] - ${release_date}$" "${changelog}"; then 91 | bail "release ${version} does not exist in ${changelog} or has wrong release date" 92 | fi 93 | if ! grep -Eq "^\\[${version//./\\.}\\]: " "${changelog}"; then 94 | bail "link to ${version} does not exist in ${changelog}" 95 | fi 96 | fi 97 | 98 | # Make sure that a valid release note for this version exists. 99 | # https://github.com/taiki-e/parse-changelog 100 | changes=$(parse-changelog "${changelog}" "${version}") 101 | if [[ -z "${changes}" ]]; then 102 | bail "changelog for ${version} has no body" 103 | fi 104 | printf '============== CHANGELOG ==============\n' 105 | printf '%s\n' "${changes}" 106 | printf '=======================================\n' 107 | 108 | metadata=$(cargo metadata --format-version=1 --no-deps) 109 | prev_version='' 110 | docs=() 111 | for readme in $(git ls-files '*README.md'); do 112 | docs+=("${readme}") 113 | lib="$(dirname -- "${readme}")/src/lib.rs" 114 | if [[ -f "${lib}" ]]; then 115 | docs+=("${lib}") 116 | fi 117 | done 118 | changed_paths=("${changelog}" "${docs[@]}") 119 | # Publishing is unrestricted if null, and forbidden if an empty array. 120 | for pkg in $(jq -c '. as $metadata | .workspace_members[] as $id | $metadata.packages[] | select(.id == $id and .publish != [])' <<<"${metadata}"); do 121 | eval "$(jq -r '@sh "NAME=\(.name) ACTUAL_VERSION=\(.version) manifest_path=\(.manifest_path)"' <<<"${pkg}")" 122 | if [[ -z "${prev_version}" ]]; then 123 | prev_version="${ACTUAL_VERSION}" 124 | fi 125 | # Make sure that the version number of all publishable workspace members matches. 126 | if [[ "${ACTUAL_VERSION}" != "${prev_version}" ]]; then 127 | bail "publishable workspace members must be version '${prev_version}', but package '${NAME}' is version '${ACTUAL_VERSION}'" 128 | fi 129 | 130 | changed_paths+=("${manifest_path}") 131 | # Update version in Cargo.toml. 132 | if ! grep -Eq "^version = \"${prev_version}\" #publish:version" "${manifest_path}"; then 133 | bail "not found '#publish:version' in version in ${manifest_path}" 134 | fi 135 | sed -E "${in_place[@]}" "s/^version = \"${prev_version}\" #publish:version/version = \"${version}\" #publish:version/g" "${manifest_path}" 136 | # Update '=' requirement in Cargo.toml. 137 | for manifest in $(git ls-files '*Cargo.toml'); do 138 | if grep -Eq "^${NAME} = \\{ version = \"=${prev_version}\"" "${manifest}"; then 139 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"=${prev_version}\"/${NAME} = { version = \"=${version}\"/g" "${manifest}" 140 | fi 141 | done 142 | # Update version in readme and lib.rs. 143 | for path in "${docs[@]}"; do 144 | # TODO: handle pre-release 145 | if [[ "${version}" == "0.0."* ]]; then 146 | # 0.0.x -> 0.0.y 147 | if grep -Eq "^${NAME} = \"${prev_version}\"" "${path}"; then 148 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_version}\"/${NAME} = \"${version}\"/g" "${path}" 149 | fi 150 | if grep -Eq "^${NAME} = \\{ version = \"${prev_version}\"" "${path}"; then 151 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_version}\"/${NAME} = { version = \"${version}\"/g" "${path}" 152 | fi 153 | elif [[ "${version}" == "0."* ]]; then 154 | prev_major_minor="${prev_version%.*}" 155 | major_minor="${version%.*}" 156 | if [[ "${prev_major_minor}" != "${major_minor}" ]]; then 157 | # 0.x -> 0.y 158 | # 0.x.* -> 0.y 159 | if grep -Eq "^${NAME} = \"${prev_major_minor}(\\.[0-9]+)?\"" "${path}"; then 160 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_major_minor}(\\.[0-9]+)?\"/${NAME} = \"${major_minor}\"/g" "${path}" 161 | fi 162 | if grep -Eq "^${NAME} = \\{ version = \"${prev_major_minor}(\\.[0-9]+)?\"" "${path}"; then 163 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_major_minor}(\\.[0-9]+)?\"/${NAME} = { version = \"${major_minor}\"/g" "${path}" 164 | fi 165 | fi 166 | else 167 | prev_major="${prev_version%%.*}" 168 | major="${version%%.*}" 169 | if [[ "${prev_major}" != "${major}" ]]; then 170 | # x -> y 171 | # x.* -> y 172 | # x.*.* -> y 173 | if grep -Eq "^${NAME} = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"" "${path}"; then 174 | sed -E "${in_place[@]}" "s/^${NAME} = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"/${NAME} = \"${major}\"/g" "${path}" 175 | fi 176 | if grep -Eq "^${NAME} = \\{ version = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"" "${path}"; then 177 | sed -E "${in_place[@]}" "s/^${NAME} = \\{ version = \"${prev_major}(\\.[0-9]+(\\.[0-9]+)?)?\"/${NAME} = { version = \"${major}\"/g" "${path}" 178 | fi 179 | fi 180 | fi 181 | done 182 | done 183 | 184 | if [[ -n "${tags}" ]]; then 185 | # Create a release commit. 186 | ( 187 | set -x 188 | git add "${changed_paths[@]}" 189 | git commit -m "Release ${version}" 190 | ) 191 | fi 192 | 193 | set -x 194 | 195 | git tag "${tag}" 196 | retry git push origin main 197 | retry git push origin --tags 198 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /tools/tidy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # SPDX-License-Identifier: Apache-2.0 OR MIT 3 | # shellcheck disable=SC2046 4 | set -CeEuo pipefail 5 | IFS=$'\n\t' 6 | trap -- 's=$?; printf >&2 "%s\n" "${0##*/}:${LINENO}: \`${BASH_COMMAND}\` exit with ${s}"; exit ${s}' ERR 7 | trap -- 'printf >&2 "%s\n" "${0##*/}: trapped SIGINT"; exit 1' SIGINT 8 | cd -- "$(dirname -- "$0")"/.. 9 | 10 | # USAGE: 11 | # ./tools/tidy.sh 12 | # 13 | # Note: This script requires the following tools: 14 | # - git 15 | # - jq 1.6+ 16 | # - npm (node 18+) 17 | # - python 3.6+ 18 | # - shfmt 19 | # - shellcheck 20 | # - cargo, rustfmt (if Rust code exists) 21 | # - clang-format (if C/C++ code exists) 22 | # 23 | # This script is shared with other repositories, so there may also be 24 | # checks for files not included in this repository, but they will be 25 | # skipped if the corresponding files do not exist. 26 | 27 | check_diff() { 28 | if [[ -n "${CI:-}" ]]; then 29 | if ! git --no-pager diff --exit-code "$@"; then 30 | should_fail=1 31 | fi 32 | else 33 | if ! git --no-pager diff --exit-code "$@" &>/dev/null; then 34 | should_fail=1 35 | fi 36 | fi 37 | } 38 | check_config() { 39 | if [[ ! -e "$1" ]]; then 40 | error "could not found $1 in the repository root" 41 | fi 42 | } 43 | check_install() { 44 | for tool in "$@"; do 45 | if ! type -P "${tool}" >/dev/null; then 46 | if [[ "${tool}" == "python3" ]]; then 47 | if type -P python >/dev/null; then 48 | continue 49 | fi 50 | fi 51 | error "'${tool}' is required to run this check" 52 | return 1 53 | fi 54 | done 55 | } 56 | retry() { 57 | for i in {1..10}; do 58 | if "$@"; then 59 | return 0 60 | else 61 | sleep "${i}" 62 | fi 63 | done 64 | "$@" 65 | } 66 | error() { 67 | if [[ -n "${GITHUB_ACTIONS:-}" ]]; then 68 | printf '::error::%s\n' "$*" 69 | else 70 | printf >&2 'error: %s\n' "$*" 71 | fi 72 | should_fail=1 73 | } 74 | warn() { 75 | if [[ -n "${GITHUB_ACTIONS:-}" ]]; then 76 | printf '::warning::%s\n' "$*" 77 | else 78 | printf >&2 'warning: %s\n' "$*" 79 | fi 80 | } 81 | info() { 82 | printf >&2 'info: %s\n' "$*" 83 | } 84 | sed_rhs_escape() { 85 | sed 's/\\/\\\\/g; s/\&/\\\&/g; s/\//\\\//g' <<<"$1" 86 | } 87 | venv_install_yq() { 88 | if [[ ! -e "${venv_bin}/yq${exe}" ]]; then 89 | if [[ ! -d .venv ]]; then 90 | "python${py_suffix}" -m venv .venv >&2 91 | fi 92 | info "installing yq to .venv using pip${py_suffix}" 93 | "${venv_bin}/pip${py_suffix}${exe}" install yq >&2 94 | fi 95 | } 96 | 97 | if [[ $# -gt 0 ]]; then 98 | cat </dev/null; then 108 | py_suffix='3' 109 | fi 110 | venv_bin=.venv/bin 111 | yq() { 112 | venv_install_yq 113 | "${venv_bin}/yq${exe}" "$@" 114 | } 115 | tomlq() { 116 | venv_install_yq 117 | "${venv_bin}/tomlq${exe}" "$@" 118 | } 119 | case "$(uname -s)" in 120 | Linux) 121 | if [[ "$(uname -o)" == "Android" ]]; then 122 | ostype=android 123 | else 124 | ostype=linux 125 | fi 126 | ;; 127 | Darwin) ostype=macos ;; 128 | FreeBSD) ostype=freebsd ;; 129 | NetBSD) ostype=netbsd ;; 130 | OpenBSD) ostype=openbsd ;; 131 | DragonFly) ostype=dragonfly ;; 132 | SunOS) 133 | if [[ "$(/usr/bin/uname -o)" == "illumos" ]]; then 134 | ostype=illumos 135 | else 136 | ostype=solaris 137 | # Solaris /usr/bin/* are not POSIX-compliant (e.g., grep has no -q, -E, -F), 138 | # and POSIX-compliant commands are in /usr/xpg{4,6,7}/bin. 139 | # https://docs.oracle.com/cd/E88353_01/html/E37853/xpg-7.html 140 | if [[ "${PATH}" != *"/usr/xpg4/bin"* ]]; then 141 | export PATH="/usr/xpg4/bin:${PATH}" 142 | fi 143 | # GNU/BSD grep/sed is required to run some checks, but most checks are okay with other POSIX grep/sed. 144 | # Solaris /usr/xpg4/bin/grep has -q, -E, -F, but no -o (non-POSIX). 145 | # Solaris /usr/xpg4/bin/sed has no -E (POSIX.1-2024) yet. 146 | if type -P ggrep >/dev/null; then 147 | grep() { ggrep "$@"; } 148 | fi 149 | if type -P gsed >/dev/null; then 150 | sed() { gsed "$@"; } 151 | fi 152 | fi 153 | ;; 154 | MINGW* | MSYS* | CYGWIN* | Windows_NT) 155 | ostype=windows 156 | exe=.exe 157 | venv_bin=.venv/Scripts 158 | if type -P jq >/dev/null; then 159 | # https://github.com/jqlang/jq/issues/1854 160 | _tmp=$(jq -r .a <<<'{}') 161 | if [[ "${_tmp}" != "null" ]]; then 162 | _tmp=$(jq -b -r .a 2>/dev/null <<<'{}' || true) 163 | if [[ "${_tmp}" == "null" ]]; then 164 | jq() { command jq -b "$@"; } 165 | else 166 | jq() { command jq "$@" | tr -d '\r'; } 167 | fi 168 | yq() { 169 | venv_install_yq 170 | "${venv_bin}/yq${exe}" "$@" | tr -d '\r' 171 | } 172 | tomlq() { 173 | venv_install_yq 174 | "${venv_bin}/tomlq${exe}" "$@" | tr -d '\r' 175 | } 176 | fi 177 | fi 178 | ;; 179 | *) error "unrecognized os type '$(uname -s)' for \`\$(uname -s)\`" ;; 180 | esac 181 | 182 | check_install git 183 | exclude_from_ls_files=() 184 | while IFS=$'\n' read -r line; do exclude_from_ls_files+=("${line}"); done < <({ 185 | find . \! \( -name .git -prune \) \! \( -name target -prune \) \! \( -name .venv -prune \) \! \( -name tmp -prune \) -type l | cut -c3- 186 | git submodule status | sed 's/^.//' | cut -d' ' -f2 187 | git ls-files --deleted 188 | } | LC_ALL=C sort -u) 189 | ls_files() { 190 | comm -23 <(git ls-files "$@" | LC_ALL=C sort) <(printf '%s\n' ${exclude_from_ls_files[@]+"${exclude_from_ls_files[@]}"}) 191 | } 192 | 193 | # Rust (if exists) 194 | if [[ -n "$(ls_files '*.rs')" ]]; then 195 | info "checking Rust code style" 196 | check_config .rustfmt.toml 197 | if check_install cargo jq python3; then 198 | # `cargo fmt` cannot recognize files not included in the current workspace and modules 199 | # defined inside macros, so run rustfmt directly. 200 | # We need to use nightly rustfmt because we use the unstable formatting options of rustfmt. 201 | rustc_version=$(rustc -vV | grep -E '^release:' | cut -d' ' -f2) 202 | if [[ "${rustc_version}" =~ nightly|dev ]] || ! type -P rustup >/dev/null; then 203 | if type -P rustup >/dev/null; then 204 | retry rustup component add rustfmt &>/dev/null 205 | fi 206 | info "running \`rustfmt \$(git ls-files '*.rs')\`" 207 | rustfmt $(ls_files '*.rs') 208 | else 209 | if type -P rustup >/dev/null; then 210 | retry rustup component add rustfmt --toolchain nightly &>/dev/null 211 | fi 212 | info "running \`rustfmt +nightly \$(git ls-files '*.rs')\`" 213 | rustfmt +nightly $(ls_files '*.rs') 214 | fi 215 | check_diff $(ls_files '*.rs') 216 | cast_without_turbofish=$(grep -Fn '.cast()' $(ls_files '*.rs') || true) 217 | if [[ -n "${cast_without_turbofish}" ]]; then 218 | error "please replace \`.cast()\` with \`.cast::()\`:" 219 | printf '%s\n' "${cast_without_turbofish}" 220 | fi 221 | # Sync readme and crate-level doc. 222 | first=1 223 | for readme in $(ls_files '*README.md'); do 224 | if ! grep -Eq '^' "${readme}"; then 225 | continue 226 | fi 227 | lib="$(dirname -- "${readme}")/src/lib.rs" 228 | if [[ -n "${first}" ]]; then 229 | first='' 230 | info "checking readme and crate-level doc are synchronized" 231 | fi 232 | if ! grep -Eq '^' "${readme}"; then 233 | bail "missing '' comment in ${readme}" 234 | fi 235 | if ! grep -Eq '^' "${lib}"; then 236 | bail "missing '' comment in ${lib}" 237 | fi 238 | if ! grep -Eq '^' "${lib}"; then 239 | bail "missing '' comment in ${lib}" 240 | fi 241 | new=$(tr '\n' '\a' <"${readme}" | grep -Eo '.*') 242 | new=$(tr '\n' '\a' <"${lib}" | sed "s/.*/$(sed_rhs_escape "${new}")/" | tr '\a' '\n') 243 | printf '%s\n' "${new}" >|"${lib}" 244 | check_diff "${lib}" 245 | done 246 | # Make sure that public Rust crates don't contain executables and binaries. 247 | executables='' 248 | binaries='' 249 | metadata=$(cargo metadata --format-version=1 --no-deps) 250 | root_manifest='' 251 | if [[ -f Cargo.toml ]]; then 252 | root_manifest=$(cargo locate-project --message-format=plain --manifest-path Cargo.toml) 253 | fi 254 | exclude='' 255 | has_public_crate='' 256 | for pkg in $(jq -c '. as $metadata | .workspace_members[] as $id | $metadata.packages[] | select(.id == $id)' <<<"${metadata}"); do 257 | eval "$(jq -r '@sh "publish=\(.publish) manifest_path=\(.manifest_path)"' <<<"${pkg}")" 258 | if [[ "$(tomlq -c '.lints' "${manifest_path}")" == "null" ]]; then 259 | error "no [lints] table in ${manifest_path} please add '[lints]' with 'workspace = true'" 260 | fi 261 | # Publishing is unrestricted if null, and forbidden if an empty array. 262 | if [[ -z "${publish}" ]]; then 263 | continue 264 | fi 265 | has_public_crate=1 266 | if [[ "${manifest_path}" == "${root_manifest}" ]]; then 267 | exclude=$(tomlq -r '.package.exclude[]' "${manifest_path}") 268 | if ! grep -Eq '^/\.\*$' <<<"${exclude}"; then 269 | error "top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \"/.*\"" 270 | fi 271 | if [[ -e tools ]] && ! grep -Eq '^/tools$' <<<"${exclude}"; then 272 | error "top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \"/tools\" if it exists" 273 | fi 274 | if [[ -e target-specs ]] && ! grep -Eq '^/target-specs$' <<<"${exclude}"; then 275 | error "top-level Cargo.toml of non-virtual workspace should have 'exclude' field with \"/target-specs\" if it exists" 276 | fi 277 | fi 278 | done 279 | if [[ -n "${has_public_crate}" ]]; then 280 | info "checking public crates don't contain executables and binaries" 281 | for p in $(ls_files); do 282 | # Skip directories. 283 | if [[ -d "${p}" ]]; then 284 | continue 285 | fi 286 | # Top-level hidden files/directories and tools/* are excluded from crates.io (ensured by the above check). 287 | # TODO: fully respect exclude field in Cargo.toml. 288 | case "${p}" in 289 | .* | tools/* | target-specs/*) continue ;; 290 | esac 291 | if [[ -x "${p}" ]]; then 292 | executables+="${p}"$'\n' 293 | fi 294 | # Use diff instead of file because file treats an empty file as a binary 295 | # https://unix.stackexchange.com/questions/275516/is-there-a-convenient-way-to-classify-files-as-binary-or-text#answer-402870 296 | if { diff .gitattributes "${p}" || true; } | grep -Eq '^Binary file'; then 297 | binaries+="${p}"$'\n' 298 | fi 299 | done 300 | if [[ -n "${executables}" ]]; then 301 | error "file-permissions-check failed: executables are only allowed to be present in directories that are excluded from crates.io" 302 | printf '=======================================\n' 303 | printf '%s' "${executables}" 304 | printf '=======================================\n' 305 | fi 306 | if [[ -n "${binaries}" ]]; then 307 | error "file-permissions-check failed: binaries are only allowed to be present in directories that are excluded from crates.io" 308 | printf '=======================================\n' 309 | printf '%s' "${binaries}" 310 | printf '=======================================\n' 311 | fi 312 | fi 313 | fi 314 | elif [[ -e .rustfmt.toml ]]; then 315 | error ".rustfmt.toml is unused" 316 | fi 317 | 318 | # C/C++ (if exists) 319 | clang_format_ext=('*.c' '*.h' '*.cpp' '*.hpp') 320 | if [[ -n "$(ls_files "${clang_format_ext[@]}")" ]]; then 321 | info "checking C/C++ code style" 322 | check_config .clang-format 323 | if check_install clang-format; then 324 | IFS=' ' 325 | info "running \`clang-format -i \$(git ls-files ${clang_format_ext[*]})\`" 326 | IFS=$'\n\t' 327 | clang-format -i $(ls_files "${clang_format_ext[@]}") 328 | check_diff $(ls_files "${clang_format_ext[@]}") 329 | fi 330 | elif [[ -e .clang-format ]]; then 331 | error ".clang-format is unused" 332 | fi 333 | # https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html 334 | cpp_alt_ext=('*.cc' '*.cp' '*.cxx' '*.C' '*.CPP' '*.c++') 335 | hpp_alt_ext=('*.hh' '*.hp' '*.hxx' '*.H' '*.HPP' '*.h++') 336 | if [[ -n "$(ls_files "${cpp_alt_ext[@]}")" ]]; then 337 | error "please use '.cpp' for consistency" 338 | printf '=======================================\n' 339 | ls_files "${cpp_alt_ext[@]}" 340 | printf '=======================================\n' 341 | fi 342 | if [[ -n "$(ls_files "${hpp_alt_ext[@]}")" ]]; then 343 | error "please use '.hpp' for consistency" 344 | printf '=======================================\n' 345 | ls_files "${hpp_alt_ext[@]}" 346 | printf '=======================================\n' 347 | fi 348 | 349 | # YAML/JavaScript/JSON (if exists) 350 | prettier_ext=('*.yml' '*.yaml' '*.js' '*.json') 351 | if [[ -n "$(ls_files "${prettier_ext[@]}")" ]]; then 352 | info "checking YAML/JavaScript/JSON code style" 353 | check_config .editorconfig 354 | if [[ "${ostype}" == "solaris" ]] && [[ -n "${CI:-}" ]] && ! type -P npm >/dev/null; then 355 | warn "this check is skipped on Solaris due to no node 18+ in upstream package manager" 356 | elif check_install npm; then 357 | IFS=' ' 358 | info "running \`npx -y prettier -l -w \$(git ls-files ${prettier_ext[*]})\`" 359 | IFS=$'\n\t' 360 | npx -y prettier -l -w $(ls_files "${prettier_ext[@]}") 361 | check_diff $(ls_files "${prettier_ext[@]}") 362 | fi 363 | fi 364 | if [[ -n "$(ls_files '*.yaml' | { grep -Fv '.markdownlint-cli2.yaml' || true; })" ]]; then 365 | error "please use '.yml' instead of '.yaml' for consistency" 366 | printf '=======================================\n' 367 | ls_files '*.yaml' | { grep -Fv '.markdownlint-cli2.yaml' || true; } 368 | printf '=======================================\n' 369 | fi 370 | 371 | # TOML (if exists) 372 | if [[ -n "$(ls_files '*.toml' | { grep -Fv '.taplo.toml' || true; })" ]]; then 373 | info "checking TOML style" 374 | check_config .taplo.toml 375 | if [[ "${ostype}" == "solaris" ]] && [[ -n "${CI:-}" ]] && ! type -P npm >/dev/null; then 376 | warn "this check is skipped on Solaris due to no node 18+ in upstream package manager" 377 | elif check_install npm; then 378 | info "running \`npx -y @taplo/cli fmt \$(git ls-files '*.toml')\`" 379 | RUST_LOG=warn npx -y @taplo/cli fmt $(ls_files '*.toml') 380 | check_diff $(ls_files '*.toml') 381 | fi 382 | elif [[ -e .taplo.toml ]]; then 383 | error ".taplo.toml is unused" 384 | fi 385 | 386 | # Markdown (if exists) 387 | if [[ -n "$(ls_files '*.md')" ]]; then 388 | info "checking Markdown style" 389 | check_config .markdownlint-cli2.yaml 390 | if [[ "${ostype}" == "solaris" ]] && [[ -n "${CI:-}" ]] && ! type -P npm >/dev/null; then 391 | warn "this check is skipped on Solaris due to no node 18+ in upstream package manager" 392 | elif check_install npm; then 393 | info "running \`npx -y markdownlint-cli2 \$(git ls-files '*.md')\`" 394 | if ! npx -y markdownlint-cli2 $(ls_files '*.md'); then 395 | should_fail=1 396 | fi 397 | fi 398 | elif [[ -e .markdownlint-cli2.yaml ]]; then 399 | error ".markdownlint-cli2.yaml is unused" 400 | fi 401 | if [[ -n "$(ls_files '*.markdown')" ]]; then 402 | error "please use '.md' instead of '.markdown' for consistency" 403 | printf '=======================================\n' 404 | ls_files '*.markdown' 405 | printf '=======================================\n' 406 | fi 407 | 408 | # Shell scripts 409 | info "checking Shell scripts" 410 | shell_files=() 411 | docker_files=() 412 | bash_files=() 413 | grep_ere_files=() 414 | sed_ere_files=() 415 | for p in $(ls_files '*.sh' '*Dockerfile*'); do 416 | case "${p##*/}" in 417 | *.sh) 418 | shell_files+=("${p}") 419 | if [[ "$(head -1 "${p}")" =~ ^#!/.*bash ]]; then 420 | bash_files+=("${p}") 421 | fi 422 | ;; 423 | *Dockerfile*) 424 | docker_files+=("${p}") 425 | bash_files+=("${p}") # TODO 426 | ;; 427 | esac 428 | if grep -Eq '(^|[^0-9A-Za-z\."'\''-])(grep) -[A-Za-z]*E[^\)]' "${p}"; then 429 | grep_ere_files+=("${p}") 430 | fi 431 | if grep -Eq '(^|[^0-9A-Za-z\."'\''-])(sed) -[A-Za-z]*E[^\)]' "${p}"; then 432 | sed_ere_files+=("${p}") 433 | fi 434 | done 435 | # TODO: .cirrus.yml 436 | workflows=() 437 | actions=() 438 | if [[ -d .github/workflows ]]; then 439 | for p in .github/workflows/*.yml; do 440 | workflows+=("${p}") 441 | bash_files+=("${p}") # TODO 442 | done 443 | fi 444 | if [[ -n "$(ls_files '*action.yml')" ]]; then 445 | for p in $(ls_files '*action.yml'); do 446 | if [[ "${p##*/}" == "action.yml" ]]; then 447 | actions+=("${p}") 448 | if ! grep -Fq 'shell: sh' "${p}"; then 449 | bash_files+=("${p}") 450 | fi 451 | fi 452 | done 453 | fi 454 | # correctness 455 | res=$({ grep -En '(\[\[ .* ]]|(^|[^\$])\(\(.*\)\))( +#| *$)' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 456 | if [[ -n "${res}" ]]; then 457 | error "bare [[ ]] and (( )) may not work as intended: see https://github.com/koalaman/shellcheck/issues/2360 for more" 458 | printf '=======================================\n' 459 | printf '%s\n' "${res}" 460 | printf '=======================================\n' 461 | fi 462 | # TODO: chmod|chown 463 | res=$({ grep -En '(^|[^0-9A-Za-z\."'\''-])(basename|cat|cd|cp|dirname|ln|ls|mkdir|mv|pushd|rm|rmdir|tee|touch)( +-[0-9A-Za-z]+)* +[^<>\|-]' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 464 | if [[ -n "${res}" ]]; then 465 | error "use \`--\` before path(s): see https://github.com/koalaman/shellcheck/issues/2707 / https://github.com/koalaman/shellcheck/issues/2612 / https://github.com/koalaman/shellcheck/issues/2305 / https://github.com/koalaman/shellcheck/issues/2157 / https://github.com/koalaman/shellcheck/issues/2121 / https://github.com/koalaman/shellcheck/issues/314 for more" 466 | printf '=======================================\n' 467 | printf '%s\n' "${res}" 468 | printf '=======================================\n' 469 | fi 470 | res=$({ grep -En '(^|[^0-9A-Za-z\."'\''-])(LINES|RANDOM|PWD)=' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 471 | if [[ -n "${res}" ]]; then 472 | error "do not modify these built-in bash variables: see https://github.com/koalaman/shellcheck/issues/2160 / https://github.com/koalaman/shellcheck/issues/2559 for more" 473 | printf '=======================================\n' 474 | printf '%s\n' "${res}" 475 | printf '=======================================\n' 476 | fi 477 | # perf 478 | res=$({ grep -En '(^|[^\\])\$\((cat) ' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 479 | if [[ -n "${res}" ]]; then 480 | error "use faster \`\$(' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 493 | if [[ -n "${res}" ]]; then 494 | error "\`type -P\` doesn't output to stderr; use \`>\` instead of \`&>\`" 495 | printf '=======================================\n' 496 | printf '%s\n' "${res}" 497 | printf '=======================================\n' 498 | fi 499 | # TODO: multi-line case 500 | res=$({ grep -En '(^|[^0-9A-Za-z\."'\''-])(echo|printf )[^;)]* \|[^\|]' "${bash_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 501 | if [[ -n "${res}" ]]; then 502 | error "use faster \`<<<...\` instead of \`echo ... |\`/\`printf ... |\`: see https://github.com/koalaman/shellcheck/issues/2593 for more" 503 | printf '=======================================\n' 504 | printf '%s\n' "${res}" 505 | printf '=======================================\n' 506 | fi 507 | # style 508 | if [[ ${#grep_ere_files[@]} -gt 0 ]]; then 509 | # We intentionally do not check for occurrences in any other order (e.g., -iE, -i -E) here. 510 | # This enforces the style and makes it easier to search. 511 | res=$({ grep -En '(^|[^0-9A-Za-z\."'\''-])(grep) +([^-]|-[^EFP-]|--[^hv])' "${grep_ere_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 512 | if [[ -n "${res}" ]]; then 513 | error "please always use ERE (grep -E) instead of BRE for code consistency within a file" 514 | printf '=======================================\n' 515 | printf '%s\n' "${res}" 516 | printf '=======================================\n' 517 | fi 518 | fi 519 | if [[ ${#sed_ere_files[@]} -gt 0 ]]; then 520 | res=$({ grep -En '(^|[^0-9A-Za-z\."'\''-])(sed) +([^-]|-[^E-]|--[^hv])' "${sed_ere_files[@]}" || true; } | { grep -Ev '^[^ ]+: *(#|//)' || true; } | LC_ALL=C sort) 521 | if [[ -n "${res}" ]]; then 522 | error "please always use ERE (sed -E) instead of BRE for code consistency within a file" 523 | printf '=======================================\n' 524 | printf '%s\n' "${res}" 525 | printf '=======================================\n' 526 | fi 527 | fi 528 | if check_install shfmt; then 529 | check_config .editorconfig 530 | info "running \`shfmt -l -w \$(git ls-files '*.sh')\`" 531 | if ! shfmt -l -w "${shell_files[@]}"; then 532 | should_fail=1 533 | fi 534 | check_diff "${shell_files[@]}" 535 | fi 536 | if [[ "${ostype}" == "solaris" ]] && [[ -n "${CI:-}" ]] && ! type -P shellcheck >/dev/null; then 537 | warn "this check is skipped on Solaris due to no haskell/shellcheck in upstream package manager" 538 | elif check_install shellcheck; then 539 | check_config .shellcheckrc 540 | info "running \`shellcheck \$(git ls-files '*.sh')\`" 541 | if ! shellcheck "${shell_files[@]}"; then 542 | should_fail=1 543 | fi 544 | if [[ ${#docker_files[@]} -gt 0 ]]; then 545 | # SC2154 doesn't seem to work on dockerfile. 546 | # SC2250 may not correct on dockerfile because $v and ${v} is sometime different: https://github.com/moby/moby/issues/42863 547 | info "running \`shellcheck --shell bash --exclude SC2154,SC2250 \$(git ls-files '*Dockerfile*')\`" 548 | if ! shellcheck --shell bash --exclude SC2154,SC2250 "${docker_files[@]}"; then 549 | should_fail=1 550 | fi 551 | fi 552 | # Check scripts in other files. 553 | if [[ ${#workflows[@]} -gt 0 ]] || [[ ${#actions[@]} -gt 0 ]]; then 554 | info "running \`shellcheck --exclude SC2086,SC2096,SC2129\` for scripts in .github/workflows/*.yml and **/action.yml" 555 | if [[ "${ostype}" == "windows" ]]; then 556 | # No such file or directory: '/proc/N/fd/N' 557 | warn "this check is skipped on Windows due to upstream bug (failed to found fd created by <())" 558 | elif [[ "${ostype}" == "dragonfly" ]]; then 559 | warn "this check is skipped on DragonFly BSD due to upstream bug (hang)" 560 | elif check_install jq python3; then 561 | shellcheck_for_gha() { 562 | local text=$1 563 | local shell=$2 564 | local display_path=$3 565 | if [[ "${text}" == "null" ]]; then 566 | return 567 | fi 568 | case "${shell}" in 569 | bash* | sh*) ;; 570 | *) return ;; 571 | esac 572 | # Use python because sed doesn't support .*?. 573 | text=$( 574 | "python${py_suffix}" - <(printf '%s\n%s' "#!/usr/bin/env ${shell%' {0}'}" "${text}") </dev/null; then 719 | warn "this check is skipped on Solaris due to no node 18+ in upstream package manager" 720 | elif [[ "${ostype}" == "illumos" ]]; then 721 | warn "this check is skipped on illumos due to upstream bug (dictionaries are not loaded correctly)" 722 | elif check_install npm jq python3; then 723 | has_rust='' 724 | if [[ -n "$(ls_files '*Cargo.toml')" ]]; then 725 | has_rust=1 726 | dependencies='' 727 | for manifest_path in $(ls_files '*Cargo.toml'); do 728 | if [[ "${manifest_path}" != "Cargo.toml" ]] && [[ "$(tomlq -c '.workspace' "${manifest_path}")" == "null" ]]; then 729 | continue 730 | fi 731 | dependencies+="$(cargo metadata --format-version=1 --no-deps --manifest-path "${manifest_path}" | jq -r '. as $metadata | .workspace_members[] as $id | $metadata.packages[] | select(.id == $id) | .dependencies[].name')"$'\n' 732 | done 733 | dependencies=$(LC_ALL=C sort -f -u <<<"${dependencies//[0-9_-]/$'\n'}") 734 | fi 735 | config_old=$(<.cspell.json) 736 | config_new=$(grep -Ev '^ *//' <<<"${config_old}" | jq 'del(.dictionaries[] | select(index("organization-dictionary") | not)) | del(.dictionaryDefinitions[] | select(.name == "organization-dictionary" | not))') 737 | trap -- 'printf "%s\n" "${config_old}" >|.cspell.json; printf >&2 "%s\n" "${0##*/}: trapped SIGINT"; exit 1' SIGINT 738 | printf '%s\n' "${config_new}" >|.cspell.json 739 | dependencies_words='' 740 | if [[ -n "${has_rust}" ]]; then 741 | dependencies_words=$(npx -y cspell stdin --no-progress --no-summary --words-only --unique <<<"${dependencies}" || true) 742 | fi 743 | all_words=$(npx -y cspell --no-progress --no-summary --words-only --unique $(ls_files | { grep -Fv "${project_dictionary}" || true; }) || true) 744 | printf '%s\n' "${config_old}" >|.cspell.json 745 | trap -- 'printf >&2 "%s\n" "${0##*/}: trapped SIGINT"; exit 1' SIGINT 746 | cat >|.github/.cspell/rust-dependencies.txt <>.github/.cspell/rust-dependencies.txt <<<"${dependencies_words}"$'\n' 752 | fi 753 | if [[ -z "${REMOVE_UNUSED_WORDS:-}" ]]; then 754 | check_diff .github/.cspell/rust-dependencies.txt 755 | fi 756 | if ! grep -Fq '.github/.cspell/rust-dependencies.txt linguist-generated' .gitattributes; then 757 | error "you may want to mark .github/.cspell/rust-dependencies.txt linguist-generated" 758 | fi 759 | 760 | info "running \`npx -y cspell --no-progress --no-summary \$(git ls-files)\`" 761 | if ! npx -y cspell --no-progress --no-summary $(ls_files); then 762 | error "spellcheck failed: please fix uses of below words or add to ${project_dictionary} if correct" 763 | printf '=======================================\n' 764 | { npx -y cspell --no-progress --no-summary --words-only $(git ls-files) || true; } | LC_ALL=C sort -f -u 765 | printf '=======================================\n\n' 766 | fi 767 | 768 | # Make sure the project-specific dictionary does not contain duplicated words. 769 | for dictionary in .github/.cspell/*.txt; do 770 | if [[ "${dictionary}" == "${project_dictionary}" ]]; then 771 | continue 772 | fi 773 | case "${ostype}" in 774 | # NetBSD uniq doesn't support -i flag. 775 | netbsd) dup=$(sed '/^$/d; /^\/\//d' "${project_dictionary}" "${dictionary}" | LC_ALL=C sort -f | tr '[:upper:]' '[:lower:]' | LC_ALL=C uniq -d) ;; 776 | *) dup=$(sed '/^$/d; /^\/\//d' "${project_dictionary}" "${dictionary}" | LC_ALL=C sort -f | LC_ALL=C uniq -d -i) ;; 777 | esac 778 | if [[ -n "${dup}" ]]; then 779 | error "duplicated words in dictionaries; please remove the following words from ${project_dictionary}" 780 | printf '=======================================\n' 781 | printf '%s\n' "${dup}" 782 | printf '=======================================\n\n' 783 | fi 784 | done 785 | 786 | # Make sure the project-specific dictionary does not contain unused words. 787 | if [[ -n "${REMOVE_UNUSED_WORDS:-}" ]]; then 788 | grep_args=() 789 | for word in $(grep -Ev '^//.*' "${project_dictionary}" || true); do 790 | if ! grep -Eqi "^${word}$" <<<"${all_words}"; then 791 | grep_args+=(-e "^${word}$") 792 | fi 793 | done 794 | if [[ ${#grep_args[@]} -gt 0 ]]; then 795 | info "removing unused words from ${project_dictionary}" 796 | res=$(grep -Ev "${grep_args[@]}" "${project_dictionary}") 797 | printf '%s\n' "${res}" >|"${project_dictionary}" 798 | fi 799 | else 800 | unused='' 801 | for word in $(grep -Ev '^//.*' "${project_dictionary}" || true); do 802 | if ! grep -Eqi "^${word}$" <<<"${all_words}"; then 803 | unused+="${word}"$'\n' 804 | fi 805 | done 806 | if [[ -n "${unused}" ]]; then 807 | error "unused words in dictionaries; please remove the following words from ${project_dictionary} or run ${0##*/} with REMOVE_UNUSED_WORDS=1" 808 | printf '=======================================\n' 809 | printf '%s' "${unused}" 810 | printf '=======================================\n' 811 | fi 812 | fi 813 | fi 814 | fi 815 | 816 | if [[ -n "${should_fail:-}" ]]; then 817 | exit 1 818 | fi 819 | --------------------------------------------------------------------------------