├── .cargo └── config ├── .config └── nextest.toml ├── .github ├── actionlint.yaml └── workflows │ ├── pr.yaml │ ├── release.yaml │ └── upgrade.yaml ├── .gitignore ├── .trunk ├── .gitignore ├── config │ ├── .markdownlint.yaml │ ├── .shellcheckrc │ ├── .yamllint.yaml │ └── toolbox.toml ├── setup-ci │ └── action.yaml └── trunk.yaml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── log4rs.yaml ├── rust-toolchain.toml ├── rustfmt.toml ├── src ├── config.rs ├── diagnostic.rs ├── git.rs ├── lib.rs ├── main.rs ├── rules │ ├── if_change_then_change.rs │ ├── mod.rs │ ├── never_edit.rs │ ├── no_curly_quotes.rs │ └── pls_no_land.rs └── run.rs ├── tests ├── do_not_land_test.rs ├── foo.bar ├── if_change_then_change │ ├── basic_ictc.file │ ├── multiple_ictc.file │ └── no_ictc.file ├── if_change_then_change_test.rs ├── integration_testing.rs ├── main_test.rs ├── never_edit_test.rs ├── no_curly_quote_test.rs ├── output_format_test.rs ├── todo_test.rs └── trunk-logo.png └── toolbox-latest /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-gnu] 2 | linker = "aarch64-linux-gnu-gcc" 3 | -------------------------------------------------------------------------------- /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [store] 2 | dir = "target/nextest" 3 | 4 | # This section defines the default nextest profile. Custom profiles are layered 5 | # on top of the default profile. 6 | [profile.default] 7 | retries = 0 8 | test-threads = "num-cpus" 9 | threads-required = 1 10 | status-level = "pass" 11 | final-status-level = "flaky" 12 | failure-output = "immediate" 13 | success-output = "never" 14 | fail-fast = true 15 | slow-timeout = { period = "60s" } 16 | 17 | [profile.ci] 18 | fail-fast = false 19 | 20 | [profile.ci.junit] 21 | path = "junit.xml" 22 | 23 | report-name = "nextest-run" 24 | store-success-output = false 25 | store-failure-output = true 26 | -------------------------------------------------------------------------------- /.github/actionlint.yaml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | labels: 3 | - mint 4 | - small 5 | - 2xlarge 6 | - prod 7 | - playwright 8 | - monorepo-amd64-large 9 | - monorepo-amd64-xlarge 10 | - monorepo-amd64-2xlarge 11 | - monorepo-arm64-2xlarge 12 | - public-amd64-2xlarge 13 | - ubuntu-x64 14 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: [pull_request] 3 | concurrency: 4 | group: ${{ github.head_ref }} 5 | cancel-in-progress: true 6 | 7 | jobs: 8 | build_and_test: 9 | name: Cargo Test [linux] 10 | runs-on: [self-hosted, ubuntu-x64] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: dtolnay/rust-toolchain@stable 15 | 16 | - name: Install nextest 17 | run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin 18 | 19 | - name: Run tests 20 | run: cargo nextest run --profile=ci 21 | 22 | - name: Upload test results 23 | # Run this step even if the test step ahead fails 24 | if: "!cancelled()" 25 | uses: trunk-io/analytics-uploader@main 26 | with: 27 | junit-paths: ${{ github.workspace }}/target/nextest/ci/*junit.xml 28 | org-slug: trunk 29 | token: ${{ secrets.TRUNK_PROD_ORG_API_TOKEN }} 30 | continue-on-error: true 31 | 32 | trunk_check_runner: 33 | name: Trunk Check runner [linux] 34 | runs-on: [self-hosted, ubuntu-x64] 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Trunk Check 40 | uses: trunk-io/trunk-action@v1 41 | with: 42 | cache: false 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Toolbox 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release_tag: 6 | type: string 7 | description: Tag to create 8 | concurrency: ${{ github.workflow }} 9 | 10 | jobs: 11 | build: 12 | name: Build ${{ matrix.target }} 13 | strategy: 14 | matrix: 15 | include: 16 | - target: x86_64-unknown-linux-gnu 17 | runs-on: [ubuntu-20.04] 18 | - target: aarch64-unknown-linux-gnu 19 | runs-on: [ubuntu-20.04] 20 | - target: x86_64-apple-darwin 21 | runs-on: [macos-12] 22 | - target: aarch64-apple-darwin 23 | runs-on: [macos-12] 24 | runs-on: ${{ matrix.runs-on }} 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: dtolnay/rust-toolchain@stable 30 | 31 | - name: Set up toolchains 32 | run: | 33 | rustup target add ${{ matrix.target }} 34 | if [[ ${{ matrix.target }} == "aarch64-unknown-linux-gnu" ]]; then 35 | sudo apt update; 36 | sudo apt install -y binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu; 37 | fi 38 | 39 | - name: Build --release 40 | run: cargo build --release --target ${{ matrix.target }} 41 | env: 42 | HORTON_RELEASE: ${{ github.event.inputs.release_tag }} 43 | 44 | - uses: actions/upload-artifact@v4 45 | with: 46 | name: ${{ matrix.target }} 47 | path: target/${{ matrix.target }}/release/trunk-toolbox 48 | 49 | tag_and_release: 50 | name: Tag and Release [ ${{ github.event.inputs.release_tag }} ] 51 | runs-on: [self-hosted, Linux] 52 | needs: [build] 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | 57 | - id: download 58 | uses: actions/download-artifact@v4 59 | with: 60 | path: build 61 | 62 | - name: Compress binaries 63 | run: | 64 | for target in $(ls build) 65 | do 66 | chmod u+x build/${target}/trunk-toolbox 67 | tar czvf \ 68 | build/trunk-toolbox-${{ github.event.inputs.release_tag }}-${target}.tar.gz \ 69 | -C build/${target} trunk-toolbox 70 | done 71 | 72 | - name: Install gh binary 73 | uses: trunk-io/trunk-action/install@v1 74 | with: 75 | tools: gh 76 | 77 | - name: Create GH release and upload binary 78 | env: 79 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | run: | 81 | gh release create --target ${{ github.ref }} --generate-notes \ 82 | ${{ github.event.inputs.release_tag }} ./build/*.tar.gz 83 | -------------------------------------------------------------------------------- /.github/workflows/upgrade.yaml: -------------------------------------------------------------------------------- 1 | name: Upgrade trunk 2 | on: 3 | # schedule: 4 | # - cron: 0 8 * * 1-5 5 | workflow_dispatch: {} 6 | permissions: read-all 7 | jobs: 8 | trunk_upgrade: 9 | name: Upgrade Trunk 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Trunk Upgrade 18 | uses: trunk-io/trunk-action/upgrade@v1 19 | with: 20 | reviewers: pat-trunk-io 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | toolbox.log -------------------------------------------------------------------------------- /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | *logs 3 | *actions 4 | *notifications 5 | *tools 6 | plugins 7 | user_trunk.yaml 8 | user.yaml 9 | tmp 10 | -------------------------------------------------------------------------------- /.trunk/config/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Prettier-friendly minimal markdownlint config (turns off rules which prettier handles) 2 | default: true 3 | MD013: false 4 | MD032: false 5 | MD033: false 6 | -------------------------------------------------------------------------------- /.trunk/config/.shellcheckrc: -------------------------------------------------------------------------------- 1 | enable=all 2 | source-path=SCRIPTDIR 3 | disable=SC2154 4 | 5 | # If you're having issues with shellcheck following source, disable the errors via: 6 | # disable=SC1090 7 | # disable=SC1091 8 | -------------------------------------------------------------------------------- /.trunk/config/.yamllint.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | quoted-strings: 3 | required: only-when-needed 4 | extra-allowed: ["{|}"] 5 | empty-values: 6 | forbid-in-block-mappings: true 7 | forbid-in-flow-mappings: true 8 | key-duplicates: {} 9 | octal-values: 10 | forbid-implicit-octal: true 11 | -------------------------------------------------------------------------------- /.trunk/config/toolbox.toml: -------------------------------------------------------------------------------- 1 | # Configuration for trunk toolbox. Generate default by calling 'trunk-toolbox genconfig' 2 | 3 | [ifchange] 4 | enabled = false 5 | 6 | [donotland] 7 | enabled = false 8 | 9 | [todo] 10 | enabled = false 11 | 12 | [neveredit] 13 | enabled = false 14 | paths = [] 15 | 16 | [nocurlyquotes] 17 | enabled = false 18 | -------------------------------------------------------------------------------- /.trunk/setup-ci/action.yaml: -------------------------------------------------------------------------------- 1 | name: trunk-io/trunk setup for trunk check/upgrade 2 | description: Set up 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: dtolnay/rust-toolchain@stable 8 | 9 | - name: Build trunk-toolbox 10 | shell: bash 11 | run: cargo build 12 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | cli: 3 | version: 1.22.8 4 | api: 5 | address: api.trunk-staging.io:8443 6 | plugins: 7 | sources: 8 | - id: trunk 9 | ref: v1.6.6 10 | uri: https://github.com/trunk-io/plugins 11 | runtimes: 12 | enabled: 13 | - python@3.10.8 14 | - go@1.21.0 15 | - node@18.20.5 16 | - rust@1.82.0 17 | tools: 18 | enabled: 19 | - gh@2.65.0 20 | runtimes: 21 | - rust 22 | lint: 23 | definitions: 24 | - name: trunk-toolbox 25 | tools: [trunk-toolbox] 26 | files: [ALL] 27 | commands: 28 | - name: lint 29 | run: ${workspace}/toolbox-latest --upstream=${upstream-ref} --cache-dir=${cachedir} --results=${tmpfile} ${target} 30 | output: sarif 31 | batch: true 32 | success_codes: [0] 33 | read_output_from: tmp_file 34 | cache_results: true 35 | disable_upstream: false 36 | max_concurrency: 1 37 | direct_configs: [toolbox.toml] 38 | version_command: 39 | parse_regex: ${semver} 40 | run: trunk-toolbox --version 41 | affects_cache: [toolbox.toml, log4rs.yaml] 42 | environment: 43 | - name: PATH 44 | list: ["${linter}", "${env.PATH}"] 45 | - name: debug_path 46 | value: "${workspace}/target/debug/trunk-toolbox" 47 | - name: release_path 48 | value: "${workspace}/target/release/trunk-toolbox" 49 | ignore: 50 | - linters: [trunk-toolbox] 51 | paths: 52 | - tests/** 53 | 54 | enabled: 55 | - osv-scanner@1.9.2 56 | - shellcheck@0.10.0 57 | - shfmt@3.6.0 58 | - trunk-toolbox@0.5.4 59 | - checkov@3.2.347 60 | - trufflehog@3.88.1 61 | - oxipng@9.1.3 62 | - yamllint@1.35.1 63 | - git-diff-check 64 | - taplo@0.9.3 65 | - actionlint@1.7.6 66 | - clippy@1.76.0 67 | - gitleaks@8.22.1 68 | - markdownlint@0.43.0 69 | - prettier@3.4.2 70 | - rustfmt@1.76.0 71 | actions: 72 | enabled: 73 | - trunk-upgrade-available 74 | - trunk-announce 75 | - trunk-check-pre-push 76 | - trunk-fmt-pre-commit 77 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "trunk.io", 4 | "vadimcn.vscode-lldb", 5 | "rust-lang.rust-analyzer" 6 | ], 7 | "unwantedRecommendations": ["rust-lang.rust"] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug toolbox", 11 | "cargo": { 12 | "args": ["build", "--bin=trunk-toolbox"], 13 | "filter": { 14 | "name": "trunk-toolbox", 15 | "kind": "bin" 16 | } 17 | }, 18 | "args": ["tests/if_change_then_change/basic_ictc.file"], 19 | "cwd": "${workspaceFolder}" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "trunk.io", 3 | "files.exclude": { 4 | "**/.trunk/*out": true, 5 | "target/**": true 6 | }, 7 | "files.watcherExclude": { 8 | "**/.trunk/*out": true, 9 | "target/**": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | Thanks for contributing to the trunk toolbox! Read on to learn more. 4 | 5 | - [Overview](#overview) 6 | - [Development](#development) 7 | - [Testing](#testing) 8 | - [Guidelines](#guidelines) 9 | - [Docs](https://docs.trunk.io) 10 | 11 | ## Overview 12 | 13 | Trunk toolbox is a place for rules that transcend particular languages and are relevant to any code in a repo. 14 | 15 | ## Development 16 | 17 | The trunk.yaml in this repo has been modified to run your local iteration of toolbox as you are building. This is managed through the `toolbox-latest` script. Effectively as you run `cargo build` or `cargo build release` the script will pick up the last built binary and use that. 18 | 19 | If no local binary has been built then the pinned version in the trunk.yaml will be used. 20 | 21 | ## Testing 22 | 23 | `cargo test` will execute the unit and integration tests for the repo 24 | 25 | ## Guidelines 26 | 27 | Please follow the guidelines below when contributing: 28 | 29 | - After defining a rule, please add it to [`README.md`](README.md). 30 | - If you run into any problems while defining a rule, feel free to reach out on our 31 | [Slack](https://slack.trunk.io/). 32 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | dependencies = [ 11 | "lazy_static", 12 | "regex", 13 | ] 14 | 15 | [[package]] 16 | name = "aho-corasick" 17 | version = "1.1.3" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 20 | dependencies = [ 21 | "memchr", 22 | ] 23 | 24 | [[package]] 25 | name = "android-tzdata" 26 | version = "0.1.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 29 | 30 | [[package]] 31 | name = "android_system_properties" 32 | version = "0.1.5" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 35 | dependencies = [ 36 | "libc", 37 | ] 38 | 39 | [[package]] 40 | name = "anstream" 41 | version = "0.6.17" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" 44 | dependencies = [ 45 | "anstyle", 46 | "anstyle-parse", 47 | "anstyle-query", 48 | "anstyle-wincon", 49 | "colorchoice", 50 | "is_terminal_polyfill", 51 | "utf8parse", 52 | ] 53 | 54 | [[package]] 55 | name = "anstyle" 56 | version = "1.0.9" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" 59 | 60 | [[package]] 61 | name = "anstyle-parse" 62 | version = "0.2.6" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 65 | dependencies = [ 66 | "utf8parse", 67 | ] 68 | 69 | [[package]] 70 | name = "anstyle-query" 71 | version = "1.1.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 74 | dependencies = [ 75 | "windows-sys 0.59.0", 76 | ] 77 | 78 | [[package]] 79 | name = "anstyle-wincon" 80 | version = "3.0.6" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 83 | dependencies = [ 84 | "anstyle", 85 | "windows-sys 0.59.0", 86 | ] 87 | 88 | [[package]] 89 | name = "anyhow" 90 | version = "1.0.91" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" 93 | 94 | [[package]] 95 | name = "arc-swap" 96 | version = "1.7.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 99 | 100 | [[package]] 101 | name = "assert_cmd" 102 | version = "2.0.16" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" 105 | dependencies = [ 106 | "anstyle", 107 | "bstr", 108 | "doc-comment", 109 | "libc", 110 | "predicates", 111 | "predicates-core", 112 | "predicates-tree", 113 | "wait-timeout", 114 | ] 115 | 116 | [[package]] 117 | name = "atty" 118 | version = "0.2.14" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 121 | dependencies = [ 122 | "hermit-abi", 123 | "libc", 124 | "winapi", 125 | ] 126 | 127 | [[package]] 128 | name = "autocfg" 129 | version = "1.4.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 132 | 133 | [[package]] 134 | name = "bitflags" 135 | version = "2.6.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 138 | 139 | [[package]] 140 | name = "block-buffer" 141 | version = "0.10.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 144 | dependencies = [ 145 | "generic-array", 146 | ] 147 | 148 | [[package]] 149 | name = "bstr" 150 | version = "1.10.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" 153 | dependencies = [ 154 | "memchr", 155 | "regex-automata", 156 | "serde", 157 | ] 158 | 159 | [[package]] 160 | name = "bumpalo" 161 | version = "3.16.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 164 | 165 | [[package]] 166 | name = "byteorder" 167 | version = "1.5.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 170 | 171 | [[package]] 172 | name = "cc" 173 | version = "1.1.31" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" 176 | dependencies = [ 177 | "jobserver", 178 | "libc", 179 | "shlex", 180 | ] 181 | 182 | [[package]] 183 | name = "cfg-if" 184 | version = "1.0.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 187 | 188 | [[package]] 189 | name = "chrono" 190 | version = "0.4.38" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 193 | dependencies = [ 194 | "android-tzdata", 195 | "iana-time-zone", 196 | "js-sys", 197 | "num-traits", 198 | "wasm-bindgen", 199 | "windows-targets", 200 | ] 201 | 202 | [[package]] 203 | name = "clap" 204 | version = "4.5.20" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 207 | dependencies = [ 208 | "clap_builder", 209 | "clap_derive", 210 | ] 211 | 212 | [[package]] 213 | name = "clap_builder" 214 | version = "4.5.20" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 217 | dependencies = [ 218 | "anstream", 219 | "anstyle", 220 | "clap_lex", 221 | "strsim 0.11.1", 222 | ] 223 | 224 | [[package]] 225 | name = "clap_derive" 226 | version = "4.5.18" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 229 | dependencies = [ 230 | "heck 0.5.0", 231 | "proc-macro2", 232 | "quote", 233 | "syn 2.0.85", 234 | ] 235 | 236 | [[package]] 237 | name = "clap_lex" 238 | version = "0.7.2" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 241 | 242 | [[package]] 243 | name = "colorchoice" 244 | version = "1.0.3" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 247 | 248 | [[package]] 249 | name = "confique" 250 | version = "0.2.6" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "12d6d568e8e1848c6e7df28ca716ab21f3d61c48760102c1848b5e0299793069" 253 | dependencies = [ 254 | "confique-macro", 255 | "json5", 256 | "serde", 257 | "serde_yaml", 258 | "toml 0.8.19", 259 | ] 260 | 261 | [[package]] 262 | name = "confique-macro" 263 | version = "0.0.10" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "e367f4d469a9fb1c3da5d4983f445c26dffa20eb293552287e8e9389e028e7dc" 266 | dependencies = [ 267 | "heck 0.3.3", 268 | "proc-macro2", 269 | "quote", 270 | "syn 1.0.109", 271 | ] 272 | 273 | [[package]] 274 | name = "content_inspector" 275 | version = "0.2.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38" 278 | dependencies = [ 279 | "memchr", 280 | ] 281 | 282 | [[package]] 283 | name = "core-foundation-sys" 284 | version = "0.8.7" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 287 | 288 | [[package]] 289 | name = "cpufeatures" 290 | version = "0.2.14" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 293 | dependencies = [ 294 | "libc", 295 | ] 296 | 297 | [[package]] 298 | name = "crossbeam-deque" 299 | version = "0.8.5" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 302 | dependencies = [ 303 | "crossbeam-epoch", 304 | "crossbeam-utils", 305 | ] 306 | 307 | [[package]] 308 | name = "crossbeam-epoch" 309 | version = "0.9.18" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 312 | dependencies = [ 313 | "crossbeam-utils", 314 | ] 315 | 316 | [[package]] 317 | name = "crossbeam-utils" 318 | version = "0.8.20" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 321 | 322 | [[package]] 323 | name = "crypto-common" 324 | version = "0.1.6" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 327 | dependencies = [ 328 | "generic-array", 329 | "typenum", 330 | ] 331 | 332 | [[package]] 333 | name = "darling" 334 | version = "0.14.4" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" 337 | dependencies = [ 338 | "darling_core", 339 | "darling_macro", 340 | ] 341 | 342 | [[package]] 343 | name = "darling_core" 344 | version = "0.14.4" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" 347 | dependencies = [ 348 | "fnv", 349 | "ident_case", 350 | "proc-macro2", 351 | "quote", 352 | "strsim 0.10.0", 353 | "syn 1.0.109", 354 | ] 355 | 356 | [[package]] 357 | name = "darling_macro" 358 | version = "0.14.4" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" 361 | dependencies = [ 362 | "darling_core", 363 | "quote", 364 | "syn 1.0.109", 365 | ] 366 | 367 | [[package]] 368 | name = "derivative" 369 | version = "2.2.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 372 | dependencies = [ 373 | "proc-macro2", 374 | "quote", 375 | "syn 1.0.109", 376 | ] 377 | 378 | [[package]] 379 | name = "derive_builder" 380 | version = "0.12.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" 383 | dependencies = [ 384 | "derive_builder_macro", 385 | ] 386 | 387 | [[package]] 388 | name = "derive_builder_core" 389 | version = "0.12.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" 392 | dependencies = [ 393 | "darling", 394 | "proc-macro2", 395 | "quote", 396 | "syn 1.0.109", 397 | ] 398 | 399 | [[package]] 400 | name = "derive_builder_macro" 401 | version = "0.12.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" 404 | dependencies = [ 405 | "derive_builder_core", 406 | "syn 1.0.109", 407 | ] 408 | 409 | [[package]] 410 | name = "destructure_traitobject" 411 | version = "0.2.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" 414 | 415 | [[package]] 416 | name = "difflib" 417 | version = "0.4.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 420 | 421 | [[package]] 422 | name = "digest" 423 | version = "0.10.7" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 426 | dependencies = [ 427 | "block-buffer", 428 | "crypto-common", 429 | ] 430 | 431 | [[package]] 432 | name = "doc-comment" 433 | version = "0.3.3" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 436 | 437 | [[package]] 438 | name = "either" 439 | version = "1.13.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 442 | 443 | [[package]] 444 | name = "env_logger" 445 | version = "0.9.3" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" 448 | dependencies = [ 449 | "atty", 450 | "humantime", 451 | "log", 452 | "regex", 453 | "termcolor", 454 | ] 455 | 456 | [[package]] 457 | name = "equivalent" 458 | version = "1.0.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 461 | 462 | [[package]] 463 | name = "errno" 464 | version = "0.3.9" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 467 | dependencies = [ 468 | "libc", 469 | "windows-sys 0.52.0", 470 | ] 471 | 472 | [[package]] 473 | name = "fastrand" 474 | version = "2.1.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 477 | 478 | [[package]] 479 | name = "float-cmp" 480 | version = "0.9.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 483 | dependencies = [ 484 | "num-traits", 485 | ] 486 | 487 | [[package]] 488 | name = "fnv" 489 | version = "1.0.7" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 492 | 493 | [[package]] 494 | name = "form_urlencoded" 495 | version = "1.2.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 498 | dependencies = [ 499 | "percent-encoding", 500 | ] 501 | 502 | [[package]] 503 | name = "fuchsia-cprng" 504 | version = "0.1.1" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 507 | 508 | [[package]] 509 | name = "function_name" 510 | version = "0.2.3" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "bef632c665dc6e2b99ffa4d913f7160bd902c4d3e4cb732d81dc3d221f848512" 513 | dependencies = [ 514 | "function_name-proc-macro", 515 | ] 516 | 517 | [[package]] 518 | name = "function_name-proc-macro" 519 | version = "0.2.3" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "569d2238870f92cff64fc810013b61edaf446ebcfba36b649b96bc5b4078328a" 522 | dependencies = [ 523 | "proc-macro-crate", 524 | "quote", 525 | "syn 1.0.109", 526 | ] 527 | 528 | [[package]] 529 | name = "generic-array" 530 | version = "0.14.7" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 533 | dependencies = [ 534 | "typenum", 535 | "version_check", 536 | ] 537 | 538 | [[package]] 539 | name = "getrandom" 540 | version = "0.2.15" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 543 | dependencies = [ 544 | "cfg-if", 545 | "libc", 546 | "wasi", 547 | ] 548 | 549 | [[package]] 550 | name = "git2" 551 | version = "0.19.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" 554 | dependencies = [ 555 | "bitflags", 556 | "libc", 557 | "libgit2-sys", 558 | "log", 559 | "url", 560 | ] 561 | 562 | [[package]] 563 | name = "glob" 564 | version = "0.3.1" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 567 | 568 | [[package]] 569 | name = "glob-match" 570 | version = "0.2.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" 573 | 574 | [[package]] 575 | name = "hashbrown" 576 | version = "0.15.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 579 | 580 | [[package]] 581 | name = "heck" 582 | version = "0.3.3" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 585 | dependencies = [ 586 | "unicode-segmentation", 587 | ] 588 | 589 | [[package]] 590 | name = "heck" 591 | version = "0.4.1" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 594 | 595 | [[package]] 596 | name = "heck" 597 | version = "0.5.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 600 | 601 | [[package]] 602 | name = "hermit-abi" 603 | version = "0.1.19" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 606 | dependencies = [ 607 | "libc", 608 | ] 609 | 610 | [[package]] 611 | name = "humantime" 612 | version = "2.1.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 615 | 616 | [[package]] 617 | name = "iana-time-zone" 618 | version = "0.1.61" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 621 | dependencies = [ 622 | "android_system_properties", 623 | "core-foundation-sys", 624 | "iana-time-zone-haiku", 625 | "js-sys", 626 | "wasm-bindgen", 627 | "windows-core", 628 | ] 629 | 630 | [[package]] 631 | name = "iana-time-zone-haiku" 632 | version = "0.1.2" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 635 | dependencies = [ 636 | "cc", 637 | ] 638 | 639 | [[package]] 640 | name = "ident_case" 641 | version = "1.0.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 644 | 645 | [[package]] 646 | name = "idna" 647 | version = "0.5.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 650 | dependencies = [ 651 | "unicode-bidi", 652 | "unicode-normalization", 653 | ] 654 | 655 | [[package]] 656 | name = "indexmap" 657 | version = "2.6.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 660 | dependencies = [ 661 | "equivalent", 662 | "hashbrown", 663 | ] 664 | 665 | [[package]] 666 | name = "is_terminal_polyfill" 667 | version = "1.70.1" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 670 | 671 | [[package]] 672 | name = "itoa" 673 | version = "1.0.11" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 676 | 677 | [[package]] 678 | name = "jobserver" 679 | version = "0.1.32" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 682 | dependencies = [ 683 | "libc", 684 | ] 685 | 686 | [[package]] 687 | name = "js-sys" 688 | version = "0.3.72" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 691 | dependencies = [ 692 | "wasm-bindgen", 693 | ] 694 | 695 | [[package]] 696 | name = "json5" 697 | version = "0.4.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" 700 | dependencies = [ 701 | "pest", 702 | "pest_derive", 703 | "serde", 704 | ] 705 | 706 | [[package]] 707 | name = "lazy_static" 708 | version = "1.5.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 711 | 712 | [[package]] 713 | name = "libc" 714 | version = "0.2.161" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 717 | 718 | [[package]] 719 | name = "libgit2-sys" 720 | version = "0.17.0+1.8.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" 723 | dependencies = [ 724 | "cc", 725 | "libc", 726 | "libz-sys", 727 | "pkg-config", 728 | ] 729 | 730 | [[package]] 731 | name = "libz-sys" 732 | version = "1.1.20" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" 735 | dependencies = [ 736 | "cc", 737 | "libc", 738 | "pkg-config", 739 | "vcpkg", 740 | ] 741 | 742 | [[package]] 743 | name = "linux-raw-sys" 744 | version = "0.4.14" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 747 | 748 | [[package]] 749 | name = "lock_api" 750 | version = "0.4.12" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 753 | dependencies = [ 754 | "autocfg", 755 | "scopeguard", 756 | ] 757 | 758 | [[package]] 759 | name = "log" 760 | version = "0.4.22" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 763 | dependencies = [ 764 | "serde", 765 | ] 766 | 767 | [[package]] 768 | name = "log-mdc" 769 | version = "0.1.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" 772 | 773 | [[package]] 774 | name = "log4rs" 775 | version = "1.3.0" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" 778 | dependencies = [ 779 | "anyhow", 780 | "arc-swap", 781 | "chrono", 782 | "derivative", 783 | "fnv", 784 | "humantime", 785 | "libc", 786 | "log", 787 | "log-mdc", 788 | "once_cell", 789 | "parking_lot", 790 | "rand 0.8.5", 791 | "serde", 792 | "serde-value", 793 | "serde_json", 794 | "serde_yaml", 795 | "thiserror", 796 | "thread-id", 797 | "typemap-ors", 798 | "winapi", 799 | ] 800 | 801 | [[package]] 802 | name = "memchr" 803 | version = "2.7.4" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 806 | 807 | [[package]] 808 | name = "normalize-line-endings" 809 | version = "0.3.0" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 812 | 813 | [[package]] 814 | name = "num" 815 | version = "0.1.42" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 818 | dependencies = [ 819 | "num-bigint", 820 | "num-complex", 821 | "num-integer", 822 | "num-iter", 823 | "num-rational", 824 | "num-traits", 825 | ] 826 | 827 | [[package]] 828 | name = "num-bigint" 829 | version = "0.1.44" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" 832 | dependencies = [ 833 | "num-integer", 834 | "num-traits", 835 | "rand 0.4.6", 836 | "rustc-serialize", 837 | ] 838 | 839 | [[package]] 840 | name = "num-complex" 841 | version = "0.1.43" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" 844 | dependencies = [ 845 | "num-traits", 846 | "rustc-serialize", 847 | ] 848 | 849 | [[package]] 850 | name = "num-integer" 851 | version = "0.1.46" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 854 | dependencies = [ 855 | "num-traits", 856 | ] 857 | 858 | [[package]] 859 | name = "num-iter" 860 | version = "0.1.45" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 863 | dependencies = [ 864 | "autocfg", 865 | "num-integer", 866 | "num-traits", 867 | ] 868 | 869 | [[package]] 870 | name = "num-rational" 871 | version = "0.1.42" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" 874 | dependencies = [ 875 | "num-bigint", 876 | "num-integer", 877 | "num-traits", 878 | "rustc-serialize", 879 | ] 880 | 881 | [[package]] 882 | name = "num-traits" 883 | version = "0.2.19" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 886 | dependencies = [ 887 | "autocfg", 888 | ] 889 | 890 | [[package]] 891 | name = "once_cell" 892 | version = "1.20.2" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 895 | 896 | [[package]] 897 | name = "ordered-float" 898 | version = "2.10.1" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" 901 | dependencies = [ 902 | "num-traits", 903 | ] 904 | 905 | [[package]] 906 | name = "parking_lot" 907 | version = "0.12.3" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 910 | dependencies = [ 911 | "lock_api", 912 | "parking_lot_core", 913 | ] 914 | 915 | [[package]] 916 | name = "parking_lot_core" 917 | version = "0.9.10" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 920 | dependencies = [ 921 | "cfg-if", 922 | "libc", 923 | "redox_syscall", 924 | "smallvec", 925 | "windows-targets", 926 | ] 927 | 928 | [[package]] 929 | name = "percent-encoding" 930 | version = "2.3.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 933 | 934 | [[package]] 935 | name = "pest" 936 | version = "2.7.14" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" 939 | dependencies = [ 940 | "memchr", 941 | "thiserror", 942 | "ucd-trie", 943 | ] 944 | 945 | [[package]] 946 | name = "pest_derive" 947 | version = "2.7.14" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" 950 | dependencies = [ 951 | "pest", 952 | "pest_generator", 953 | ] 954 | 955 | [[package]] 956 | name = "pest_generator" 957 | version = "2.7.14" 958 | source = "registry+https://github.com/rust-lang/crates.io-index" 959 | checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" 960 | dependencies = [ 961 | "pest", 962 | "pest_meta", 963 | "proc-macro2", 964 | "quote", 965 | "syn 2.0.85", 966 | ] 967 | 968 | [[package]] 969 | name = "pest_meta" 970 | version = "2.7.14" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" 973 | dependencies = [ 974 | "once_cell", 975 | "pest", 976 | "sha2", 977 | ] 978 | 979 | [[package]] 980 | name = "pkg-config" 981 | version = "0.3.31" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 984 | 985 | [[package]] 986 | name = "ppv-lite86" 987 | version = "0.2.20" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 990 | dependencies = [ 991 | "zerocopy", 992 | ] 993 | 994 | [[package]] 995 | name = "predicates" 996 | version = "3.1.2" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" 999 | dependencies = [ 1000 | "anstyle", 1001 | "difflib", 1002 | "float-cmp", 1003 | "normalize-line-endings", 1004 | "predicates-core", 1005 | "regex", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "predicates-core" 1010 | version = "1.0.8" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" 1013 | 1014 | [[package]] 1015 | name = "predicates-tree" 1016 | version = "1.0.11" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" 1019 | dependencies = [ 1020 | "predicates-core", 1021 | "termtree", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "prettyplease" 1026 | version = "0.1.25" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" 1029 | dependencies = [ 1030 | "proc-macro2", 1031 | "syn 1.0.109", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "proc-macro-crate" 1036 | version = "0.1.5" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" 1039 | dependencies = [ 1040 | "toml 0.5.11", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "proc-macro2" 1045 | version = "1.0.89" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 1048 | dependencies = [ 1049 | "unicode-ident", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "quote" 1054 | version = "1.0.37" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1057 | dependencies = [ 1058 | "proc-macro2", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "rand" 1063 | version = "0.4.6" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 1066 | dependencies = [ 1067 | "fuchsia-cprng", 1068 | "libc", 1069 | "rand_core 0.3.1", 1070 | "rdrand", 1071 | "winapi", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "rand" 1076 | version = "0.8.5" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1079 | dependencies = [ 1080 | "libc", 1081 | "rand_chacha", 1082 | "rand_core 0.6.4", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "rand_chacha" 1087 | version = "0.3.1" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1090 | dependencies = [ 1091 | "ppv-lite86", 1092 | "rand_core 0.6.4", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "rand_core" 1097 | version = "0.3.1" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1100 | dependencies = [ 1101 | "rand_core 0.4.2", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "rand_core" 1106 | version = "0.4.2" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1109 | 1110 | [[package]] 1111 | name = "rand_core" 1112 | version = "0.6.4" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1115 | dependencies = [ 1116 | "getrandom", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "rayon" 1121 | version = "1.10.0" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 1124 | dependencies = [ 1125 | "either", 1126 | "rayon-core", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "rayon-core" 1131 | version = "1.12.1" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 1134 | dependencies = [ 1135 | "crossbeam-deque", 1136 | "crossbeam-utils", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "rdrand" 1141 | version = "0.4.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1144 | dependencies = [ 1145 | "rand_core 0.3.1", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "redox_syscall" 1150 | version = "0.5.7" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 1153 | dependencies = [ 1154 | "bitflags", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "regex" 1159 | version = "1.11.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1162 | dependencies = [ 1163 | "aho-corasick", 1164 | "memchr", 1165 | "regex-automata", 1166 | "regex-syntax", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "regex-automata" 1171 | version = "0.4.8" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 1174 | dependencies = [ 1175 | "aho-corasick", 1176 | "memchr", 1177 | "regex-syntax", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "regex-syntax" 1182 | version = "0.8.5" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1185 | 1186 | [[package]] 1187 | name = "rustc-serialize" 1188 | version = "0.3.25" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" 1191 | 1192 | [[package]] 1193 | name = "rustix" 1194 | version = "0.38.38" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" 1197 | dependencies = [ 1198 | "bitflags", 1199 | "errno", 1200 | "libc", 1201 | "linux-raw-sys", 1202 | "windows-sys 0.52.0", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "rustversion" 1207 | version = "1.0.18" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 1210 | 1211 | [[package]] 1212 | name = "ryu" 1213 | version = "1.0.18" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1216 | 1217 | [[package]] 1218 | name = "schemafy_core" 1219 | version = "0.6.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "2bec29dddcfe60f92f3c0d422707b8b56473983ef0481df8d5236ed3ab8fdf24" 1222 | dependencies = [ 1223 | "serde", 1224 | "serde_json", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "schemafy_lib" 1229 | version = "0.6.0" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "af3d87f1df246a9b7e2bfd1f4ee5f88e48b11ef9cfc62e63f0dead255b1a6f5f" 1232 | dependencies = [ 1233 | "Inflector", 1234 | "proc-macro2", 1235 | "quote", 1236 | "schemafy_core", 1237 | "serde", 1238 | "serde_derive", 1239 | "serde_json", 1240 | "syn 1.0.109", 1241 | "uriparse", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "scopeguard" 1246 | version = "1.2.0" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1249 | 1250 | [[package]] 1251 | name = "serde" 1252 | version = "1.0.214" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 1255 | dependencies = [ 1256 | "serde_derive", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "serde-sarif" 1261 | version = "0.3.7" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "51eda7853590f74ef6f8b9d868f20679f2072b4b86c8970d5ce320b42d457198" 1264 | dependencies = [ 1265 | "anyhow", 1266 | "derive_builder", 1267 | "prettyplease", 1268 | "proc-macro2", 1269 | "quote", 1270 | "schemafy_lib", 1271 | "serde", 1272 | "serde_json", 1273 | "strum", 1274 | "strum_macros", 1275 | "syn 1.0.109", 1276 | "thiserror", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "serde-value" 1281 | version = "0.7.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" 1284 | dependencies = [ 1285 | "ordered-float", 1286 | "serde", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "serde_derive" 1291 | version = "1.0.214" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 1294 | dependencies = [ 1295 | "proc-macro2", 1296 | "quote", 1297 | "syn 2.0.85", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "serde_json" 1302 | version = "1.0.132" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 1305 | dependencies = [ 1306 | "itoa", 1307 | "memchr", 1308 | "ryu", 1309 | "serde", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "serde_spanned" 1314 | version = "0.6.8" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1317 | dependencies = [ 1318 | "serde", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "serde_yaml" 1323 | version = "0.9.34+deprecated" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 1326 | dependencies = [ 1327 | "indexmap", 1328 | "itoa", 1329 | "ryu", 1330 | "serde", 1331 | "unsafe-libyaml", 1332 | ] 1333 | 1334 | [[package]] 1335 | name = "sha2" 1336 | version = "0.10.8" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1339 | dependencies = [ 1340 | "cfg-if", 1341 | "cpufeatures", 1342 | "digest", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "shlex" 1347 | version = "1.3.0" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1350 | 1351 | [[package]] 1352 | name = "smallvec" 1353 | version = "1.13.2" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1356 | 1357 | [[package]] 1358 | name = "spectral" 1359 | version = "0.6.0" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" 1362 | dependencies = [ 1363 | "num", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "strsim" 1368 | version = "0.10.0" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1371 | 1372 | [[package]] 1373 | name = "strsim" 1374 | version = "0.11.1" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1377 | 1378 | [[package]] 1379 | name = "strum" 1380 | version = "0.24.1" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" 1383 | 1384 | [[package]] 1385 | name = "strum_macros" 1386 | version = "0.24.3" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" 1389 | dependencies = [ 1390 | "heck 0.4.1", 1391 | "proc-macro2", 1392 | "quote", 1393 | "rustversion", 1394 | "syn 1.0.109", 1395 | ] 1396 | 1397 | [[package]] 1398 | name = "syn" 1399 | version = "1.0.109" 1400 | source = "registry+https://github.com/rust-lang/crates.io-index" 1401 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1402 | dependencies = [ 1403 | "proc-macro2", 1404 | "quote", 1405 | "unicode-ident", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "syn" 1410 | version = "2.0.85" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 1413 | dependencies = [ 1414 | "proc-macro2", 1415 | "quote", 1416 | "unicode-ident", 1417 | ] 1418 | 1419 | [[package]] 1420 | name = "tempfile" 1421 | version = "3.13.0" 1422 | source = "registry+https://github.com/rust-lang/crates.io-index" 1423 | checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 1424 | dependencies = [ 1425 | "cfg-if", 1426 | "fastrand", 1427 | "once_cell", 1428 | "rustix", 1429 | "windows-sys 0.59.0", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "termcolor" 1434 | version = "1.4.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1437 | dependencies = [ 1438 | "winapi-util", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "termtree" 1443 | version = "0.4.1" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 1446 | 1447 | [[package]] 1448 | name = "thiserror" 1449 | version = "1.0.65" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" 1452 | dependencies = [ 1453 | "thiserror-impl", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "thiserror-impl" 1458 | version = "1.0.65" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" 1461 | dependencies = [ 1462 | "proc-macro2", 1463 | "quote", 1464 | "syn 2.0.85", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "thread-id" 1469 | version = "4.2.2" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" 1472 | dependencies = [ 1473 | "libc", 1474 | "winapi", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "tinyvec" 1479 | version = "1.8.0" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1482 | dependencies = [ 1483 | "tinyvec_macros", 1484 | ] 1485 | 1486 | [[package]] 1487 | name = "tinyvec_macros" 1488 | version = "0.1.1" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1491 | 1492 | [[package]] 1493 | name = "toml" 1494 | version = "0.5.11" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 1497 | dependencies = [ 1498 | "serde", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "toml" 1503 | version = "0.8.19" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1506 | dependencies = [ 1507 | "serde", 1508 | "serde_spanned", 1509 | "toml_datetime", 1510 | "toml_edit", 1511 | ] 1512 | 1513 | [[package]] 1514 | name = "toml_datetime" 1515 | version = "0.6.8" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1518 | dependencies = [ 1519 | "serde", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "toml_edit" 1524 | version = "0.22.22" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1527 | dependencies = [ 1528 | "indexmap", 1529 | "serde", 1530 | "serde_spanned", 1531 | "toml_datetime", 1532 | "winnow", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "trunk-toolbox" 1537 | version = "0.0.0" 1538 | dependencies = [ 1539 | "anyhow", 1540 | "assert_cmd", 1541 | "chrono", 1542 | "clap", 1543 | "confique", 1544 | "content_inspector", 1545 | "env_logger", 1546 | "function_name", 1547 | "git2", 1548 | "glob", 1549 | "glob-match", 1550 | "lazy_static", 1551 | "log", 1552 | "log4rs", 1553 | "predicates", 1554 | "rayon", 1555 | "regex", 1556 | "serde", 1557 | "serde-sarif", 1558 | "serde_json", 1559 | "sha2", 1560 | "spectral", 1561 | "tempfile", 1562 | "url", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "typemap-ors" 1567 | version = "1.0.0" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" 1570 | dependencies = [ 1571 | "unsafe-any-ors", 1572 | ] 1573 | 1574 | [[package]] 1575 | name = "typenum" 1576 | version = "1.17.0" 1577 | source = "registry+https://github.com/rust-lang/crates.io-index" 1578 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1579 | 1580 | [[package]] 1581 | name = "ucd-trie" 1582 | version = "0.1.7" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1585 | 1586 | [[package]] 1587 | name = "unicode-bidi" 1588 | version = "0.3.17" 1589 | source = "registry+https://github.com/rust-lang/crates.io-index" 1590 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 1591 | 1592 | [[package]] 1593 | name = "unicode-ident" 1594 | version = "1.0.13" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1597 | 1598 | [[package]] 1599 | name = "unicode-normalization" 1600 | version = "0.1.24" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1603 | dependencies = [ 1604 | "tinyvec", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "unicode-segmentation" 1609 | version = "1.12.0" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1612 | 1613 | [[package]] 1614 | name = "unsafe-any-ors" 1615 | version = "1.0.0" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" 1618 | dependencies = [ 1619 | "destructure_traitobject", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "unsafe-libyaml" 1624 | version = "0.2.11" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 1627 | 1628 | [[package]] 1629 | name = "uriparse" 1630 | version = "0.6.4" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" 1633 | dependencies = [ 1634 | "fnv", 1635 | "lazy_static", 1636 | ] 1637 | 1638 | [[package]] 1639 | name = "url" 1640 | version = "2.5.2" 1641 | source = "registry+https://github.com/rust-lang/crates.io-index" 1642 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1643 | dependencies = [ 1644 | "form_urlencoded", 1645 | "idna", 1646 | "percent-encoding", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "utf8parse" 1651 | version = "0.2.2" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1654 | 1655 | [[package]] 1656 | name = "vcpkg" 1657 | version = "0.2.15" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1660 | 1661 | [[package]] 1662 | name = "version_check" 1663 | version = "0.9.5" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1666 | 1667 | [[package]] 1668 | name = "wait-timeout" 1669 | version = "0.2.0" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 1672 | dependencies = [ 1673 | "libc", 1674 | ] 1675 | 1676 | [[package]] 1677 | name = "wasi" 1678 | version = "0.11.0+wasi-snapshot-preview1" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1681 | 1682 | [[package]] 1683 | name = "wasm-bindgen" 1684 | version = "0.2.95" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 1687 | dependencies = [ 1688 | "cfg-if", 1689 | "once_cell", 1690 | "wasm-bindgen-macro", 1691 | ] 1692 | 1693 | [[package]] 1694 | name = "wasm-bindgen-backend" 1695 | version = "0.2.95" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 1698 | dependencies = [ 1699 | "bumpalo", 1700 | "log", 1701 | "once_cell", 1702 | "proc-macro2", 1703 | "quote", 1704 | "syn 2.0.85", 1705 | "wasm-bindgen-shared", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "wasm-bindgen-macro" 1710 | version = "0.2.95" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 1713 | dependencies = [ 1714 | "quote", 1715 | "wasm-bindgen-macro-support", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "wasm-bindgen-macro-support" 1720 | version = "0.2.95" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 1723 | dependencies = [ 1724 | "proc-macro2", 1725 | "quote", 1726 | "syn 2.0.85", 1727 | "wasm-bindgen-backend", 1728 | "wasm-bindgen-shared", 1729 | ] 1730 | 1731 | [[package]] 1732 | name = "wasm-bindgen-shared" 1733 | version = "0.2.95" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1736 | 1737 | [[package]] 1738 | name = "winapi" 1739 | version = "0.3.9" 1740 | source = "registry+https://github.com/rust-lang/crates.io-index" 1741 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1742 | dependencies = [ 1743 | "winapi-i686-pc-windows-gnu", 1744 | "winapi-x86_64-pc-windows-gnu", 1745 | ] 1746 | 1747 | [[package]] 1748 | name = "winapi-i686-pc-windows-gnu" 1749 | version = "0.4.0" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1752 | 1753 | [[package]] 1754 | name = "winapi-util" 1755 | version = "0.1.9" 1756 | source = "registry+https://github.com/rust-lang/crates.io-index" 1757 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1758 | dependencies = [ 1759 | "windows-sys 0.59.0", 1760 | ] 1761 | 1762 | [[package]] 1763 | name = "winapi-x86_64-pc-windows-gnu" 1764 | version = "0.4.0" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1767 | 1768 | [[package]] 1769 | name = "windows-core" 1770 | version = "0.52.0" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1773 | dependencies = [ 1774 | "windows-targets", 1775 | ] 1776 | 1777 | [[package]] 1778 | name = "windows-sys" 1779 | version = "0.52.0" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1782 | dependencies = [ 1783 | "windows-targets", 1784 | ] 1785 | 1786 | [[package]] 1787 | name = "windows-sys" 1788 | version = "0.59.0" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1791 | dependencies = [ 1792 | "windows-targets", 1793 | ] 1794 | 1795 | [[package]] 1796 | name = "windows-targets" 1797 | version = "0.52.6" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1800 | dependencies = [ 1801 | "windows_aarch64_gnullvm", 1802 | "windows_aarch64_msvc", 1803 | "windows_i686_gnu", 1804 | "windows_i686_gnullvm", 1805 | "windows_i686_msvc", 1806 | "windows_x86_64_gnu", 1807 | "windows_x86_64_gnullvm", 1808 | "windows_x86_64_msvc", 1809 | ] 1810 | 1811 | [[package]] 1812 | name = "windows_aarch64_gnullvm" 1813 | version = "0.52.6" 1814 | source = "registry+https://github.com/rust-lang/crates.io-index" 1815 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1816 | 1817 | [[package]] 1818 | name = "windows_aarch64_msvc" 1819 | version = "0.52.6" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1822 | 1823 | [[package]] 1824 | name = "windows_i686_gnu" 1825 | version = "0.52.6" 1826 | source = "registry+https://github.com/rust-lang/crates.io-index" 1827 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1828 | 1829 | [[package]] 1830 | name = "windows_i686_gnullvm" 1831 | version = "0.52.6" 1832 | source = "registry+https://github.com/rust-lang/crates.io-index" 1833 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1834 | 1835 | [[package]] 1836 | name = "windows_i686_msvc" 1837 | version = "0.52.6" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1840 | 1841 | [[package]] 1842 | name = "windows_x86_64_gnu" 1843 | version = "0.52.6" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1846 | 1847 | [[package]] 1848 | name = "windows_x86_64_gnullvm" 1849 | version = "0.52.6" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1852 | 1853 | [[package]] 1854 | name = "windows_x86_64_msvc" 1855 | version = "0.52.6" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1858 | 1859 | [[package]] 1860 | name = "winnow" 1861 | version = "0.6.20" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 1864 | dependencies = [ 1865 | "memchr", 1866 | ] 1867 | 1868 | [[package]] 1869 | name = "zerocopy" 1870 | version = "0.7.35" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1873 | dependencies = [ 1874 | "byteorder", 1875 | "zerocopy-derive", 1876 | ] 1877 | 1878 | [[package]] 1879 | name = "zerocopy-derive" 1880 | version = "0.7.35" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1883 | dependencies = [ 1884 | "proc-macro2", 1885 | "quote", 1886 | "syn 2.0.85", 1887 | ] 1888 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trunk-toolbox" 3 | # build.rs bypasses package.version, instead using $HORTON_RELEASE and falls back to the current ref 4 | version = "0.0.0" 5 | authors = ["horton "] 6 | license = "MIT" 7 | description = "trunk custom issue finder" 8 | readme = "README.md" 9 | edition = "2021" 10 | build = "build.rs" 11 | 12 | [lib] 13 | name = "horton" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | anyhow = "1.0.64" 18 | clap = { version = "4.0.8", features = ["derive"] } 19 | env_logger = "0.9.1" 20 | git2 = { version = "0.19", default-features = false } 21 | lazy_static = "1.4.0" 22 | log = "0.4.22" 23 | regex = "1.10.6" 24 | serde = { version = "1.0.209", features = ["derive"] } 25 | serde_json = "1.0.85" 26 | serde-sarif = "0.3.4" 27 | content_inspector = "0.2.4" 28 | rayon = "1.10.0" 29 | confique = "0.2.5" 30 | glob = "0.3.1" 31 | glob-match = "0.2.1" 32 | url = "2.5.2" 33 | sha2 = "0.10.8" 34 | chrono = "0.4.38" 35 | log4rs = "1.3.0" 36 | tempfile = "3.3.0" 37 | 38 | [dev-dependencies] 39 | assert_cmd = "2.0" 40 | function_name = "0.2.0" 41 | predicates = "3.1.2" 42 | spectral = "0.6.0" 43 | tempfile = "3.3.0" 44 | 45 | [profile.release] 46 | codegen-units = 1 47 | debug = false 48 | lto = "thin" 49 | opt-level = 3 50 | panic = "abort" 51 | 52 | [profile.dev.package."*"] 53 | opt-level = 3 54 | debug = true 55 | incremental = true 56 | 57 | [profile.dev] 58 | opt-level = 0 59 | debug = true 60 | incremental = true 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Trunk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 | ### Welcome 21 | 22 | Toolbox is our custom collection of must have tools for any large software project. We've got a backlog of small tools to built into our toolbox here and happy to take contributions as well. `toolbox` is best used through `trunk check` to keep your development on rails (not the ruby kind). 23 | 24 | This repo is open to contributions! See our 25 | [contribution guidelines](CONTRIBUTING.md) 26 | 27 | ### Enabling 28 | 29 | To enable the toolbox rules in your repository run: 30 | 31 | ```bash 32 | trunk check enable trunk-toolbox 33 | ``` 34 | 35 | ### Configuration 36 | 37 | Toolbox can be configured via the toolbox.toml file. There is an example config file [here](.config/toolbox.toml). A full example file can be generated by calling 38 | 39 | ```bash 40 | trunk-toolbox genconfig 41 | ``` 42 | 43 | ### Rules 44 | 45 | #### do-not-land 46 | 47 | ##### What it does 48 | 49 | Keeps you from accidentally commiting code to a repository that is experimental, temporary, debugging cruft. It keeps you from pushing a PR with a bunch of printf() statements you added while debugging an error. 50 | 51 | Valid triggers for this rule are: DONOTLAND, DO-NOT-LAND, DO_NOT_LAND, donotland, do-not-land, do_not_land 52 | 53 | ##### Why is this bad? 54 | 55 | Anything you intentionally don't want in your repo should really not be there. This lets you flag the code you are writing to do testing without worrying that you'll forget you dropped it in your files before pushing your Pull Request. 56 | 57 | ##### Example 58 | 59 | ```typescript 60 | // DONOTLAND 61 | console.log("I don't think this code should execute"); 62 | ``` 63 | 64 | #### TODO 65 | 66 | ##### What it does 67 | 68 | Keeps you from accidentally commiting incomplete code to your repo. This is functionally the same as DONOTLAND, but reports at a lower severity so you can customize your approach to burning them down. 69 | 70 | Valid triggers for this rule are: TODO, todo, FIXME, fixme 71 | 72 | By default, this rule is disabled and must be enabled with: 73 | 74 | ```toml 75 | [todo] 76 | enabled = true 77 | ``` 78 | 79 | ##### Why is this bad? 80 | 81 | TODOs should be treated like any other lint issue. Sometimes you need to land code that still has these issues, but you want to keep track of them and avoid them when possible. 82 | 83 | ##### Example 84 | 85 | ```typescript 86 | // TODO: We should evaluate using the asynchronous API 87 | uploadResultsSync(); 88 | ``` 89 | 90 | #### if-change-then-change 91 | 92 | ##### What it does 93 | 94 | Allows you to enforce code synchronization. Often, we have code in one file that is reliant on code in another loosely - say an enum has 4 options and you want to make sure consumers of that enum are kept in sync as new enums are added. This rule will make sure the code is updated in both places when a modification occurs to the code block. 95 | 96 | ##### Why is this bad? 97 | 98 | If code has baked-in assumptions that are not enforced through a check - then they can easily get out of sync. This rule allows you to encode that dependency and ensure all related code is updated when a modification occurs. 99 | 100 | ##### Example 101 | 102 | This rule will report a violation if picker.rs is not updated when the content inside this enum block is modified: 103 | 104 | ```rust 105 | let x = 7; 106 | 107 | // IfChange 108 | enum Flavor { 109 | Strawberry, 110 | Chocholate 111 | } 112 | // ThenChange srcs/robot/picker.rs 113 | 114 | x += 9; // why not 115 | ``` 116 | 117 | #### never-edit 118 | 119 | ##### What it does 120 | 121 | Allows you to enforce code does not get modified once checked into the repo. 122 | 123 | ##### Why is this bad? 124 | 125 | If code is immutable - like database migration scripts - you want to ensure that no one edits those files 126 | once they are checked in. This rule allows you to create restricted lists of files that cannot be edited 127 | once added to the repo. 128 | 129 | ##### Example 130 | 131 | This rule will report a violation if src/write_once.txt is modified or deleted in git given this config in toolbox.toml 132 | 133 | ```toml 134 | [neveredit] 135 | enabled = true 136 | paths = ["**/write_once*"] 137 | ``` 138 | 139 | ### Debugging Toolbox 140 | 141 | Starting with release 0.5.0 toolbox now supports logging configuration using log4rs.yaml. toolbox will attempt to load this file from 142 | the currently executing directory. An example of this file is in the root directory of this repo. 143 | 144 | ### Disclaimer 145 | 146 | We know, we know...toolbox? That's only one step above 'UTILS', but a toolbox is a real thing, you can buy one in a store and put all kind of interesting things inside of them that make doing work a lot easier. Have you ever tried to change a wall socket without a screwdriver? We have...and it's not fun. 147 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | // This is a workaround to allow the value of HORTON_RELEASE during `cargo build` to control 5 | // the built version, as opposed to pulling the package version from Cargo.toml. 6 | // See https://github.com/rust-lang/cargo/issues/6583#issuecomment-1259871885 7 | println!("cargo:rerun-if-env-changed=HORTON_RELEASE"); 8 | if let Ok(val) = std::env::var("HORTON_RELEASE") { 9 | println!("cargo:rustc-env=CARGO_PKG_VERSION={}", val); 10 | } else { 11 | let output = Command::new("git") 12 | .args(["rev-parse", "--abbrev-ref", "HEAD"]) 13 | .output() 14 | .unwrap(); 15 | let git_ref = String::from_utf8(output.stdout).unwrap(); 16 | println!("cargo:rustc-env=CARGO_PKG_VERSION={}", git_ref); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /log4rs.yaml: -------------------------------------------------------------------------------- 1 | appenders: 2 | stdout: 3 | encoder: 4 | pattern: "{d(%H:%M:%S)} | {({l}):5.5} | {f}:{L} | {m}{n}" 5 | kind: console 6 | 7 | # Appender for the step strategy 8 | file: 9 | kind: file 10 | path: toolbox.log 11 | encoder: 12 | pattern: "{d(%H:%M:%S)} | {({l}):5.5} | {f}:{L} | {m}{n}" 13 | 14 | root: 15 | level: error 16 | appenders: 17 | - stdout 18 | - file 19 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.82.0" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | use_field_init_shorthand = true 3 | newline_style = "Unix" 4 | indent_style = "Block" 5 | unstable_features = true 6 | imports_granularity = "Module" 7 | group_imports = "StdExternalCrate" 8 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // trunk-ignore-all(trunk-toolbox/do-not-land,trunk-toolbox/todo) 2 | use confique::toml::{self, FormatOptions}; 3 | use confique::Config; 4 | 5 | #[derive(Config)] 6 | pub struct Conf { 7 | #[config(nested)] 8 | pub ifchange: IfChangeConf, 9 | 10 | #[config(nested)] 11 | pub donotland: PlsNotLandConf, 12 | 13 | #[config(nested)] 14 | pub todo: TodoConf, 15 | 16 | #[config(nested)] 17 | pub neveredit: NeverEditConf, 18 | 19 | #[config(nested)] 20 | pub nocurlyquotes: NoCurlyQuotesConf, 21 | } 22 | 23 | impl Conf { 24 | pub fn print_default() { 25 | let default_config = toml::template::(FormatOptions::default()); 26 | println!("{}", default_config); 27 | } 28 | } 29 | 30 | #[derive(Config)] 31 | pub struct IfChangeConf { 32 | #[config(default = true)] 33 | pub enabled: bool, 34 | } 35 | 36 | #[derive(Config)] 37 | pub struct PlsNotLandConf { 38 | #[config(default = true)] 39 | pub enabled: bool, 40 | } 41 | 42 | #[derive(Config)] 43 | pub struct TodoConf { 44 | #[config(default = false)] 45 | pub enabled: bool, 46 | } 47 | 48 | #[derive(Config)] 49 | pub struct NeverEditConf { 50 | #[config(default = false)] 51 | pub enabled: bool, 52 | #[config(default = [])] 53 | pub paths: Vec, 54 | } 55 | 56 | #[derive(Config)] 57 | pub struct NoCurlyQuotesConf { 58 | #[config(default = false)] 59 | pub enabled: bool, 60 | } 61 | -------------------------------------------------------------------------------- /src/diagnostic.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_sarif::sarif; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Serialize)] 6 | pub enum Severity { 7 | Error, 8 | Warning, 9 | Note, 10 | None, 11 | } 12 | 13 | impl fmt::Display for Severity { 14 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 15 | match self { 16 | Severity::Error => write!(f, "error"), 17 | Severity::Warning => write!(f, "warning"), 18 | Severity::Note => write!(f, "note"), 19 | Severity::None => write!(f, "none"), 20 | } 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize)] 25 | pub struct Position { 26 | pub line: u64, 27 | pub character: u64, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize)] 31 | pub struct Range { 32 | pub start: Position, 33 | pub end: Position, 34 | } 35 | 36 | #[derive(Clone, Serialize)] 37 | pub struct Replacement { 38 | pub deleted_region: Range, 39 | pub inserted_content: String, 40 | } 41 | #[derive(Clone, Serialize)] 42 | pub struct Diagnostic { 43 | pub path: String, 44 | pub range: Option, 45 | pub severity: Severity, 46 | pub code: String, 47 | pub message: String, 48 | pub replacements: Option>, 49 | } 50 | 51 | #[derive(Serialize, Default)] 52 | pub struct Diagnostics { 53 | pub diagnostics: Vec, 54 | } 55 | 56 | impl Diagnostics { 57 | pub fn to_string(&self) -> anyhow::Result { 58 | let as_string = serde_json::to_string(&self)?; 59 | Ok(as_string) 60 | } 61 | } 62 | 63 | impl Diagnostic { 64 | pub fn to_sarif(&self) -> sarif::Result { 65 | let mut physical_location = sarif::PhysicalLocationBuilder::default(); 66 | physical_location.artifact_location( 67 | sarif::ArtifactLocationBuilder::default() 68 | .uri(self.path.clone()) 69 | .build() 70 | .unwrap(), 71 | ); 72 | 73 | if let Some(range) = &self.range { 74 | physical_location.region( 75 | sarif::RegionBuilder::default() 76 | .start_line(range.start.line as i64 + 1) 77 | .start_column(range.start.character as i64 + 1) 78 | .end_line(range.end.line as i64 + 1) 79 | .end_column(range.end.character as i64 + 1) 80 | .build() 81 | .unwrap(), 82 | ); 83 | } 84 | 85 | let fixes = if let Some(replacements) = &self.replacements { 86 | Some(vec![self.build_fix(replacements)]) 87 | } else { 88 | None 89 | }; 90 | 91 | sarif::ResultBuilder::default() 92 | .level(self.severity.to_string()) 93 | .locations([sarif::LocationBuilder::default() 94 | .physical_location(physical_location.build().unwrap()) 95 | .build() 96 | .unwrap()]) 97 | .fixes(fixes.unwrap_or_default()) 98 | .message( 99 | sarif::MessageBuilder::default() 100 | .text(self.message.clone()) 101 | .build() 102 | .unwrap(), 103 | ) 104 | .rule_id(self.code.clone()) 105 | .build() 106 | .unwrap() 107 | } 108 | 109 | pub fn build_fix(&self, replacements: &[Replacement]) -> sarif::Fix { 110 | sarif::FixBuilder::default() 111 | .artifact_changes([sarif::ArtifactChangeBuilder::default() 112 | .artifact_location( 113 | sarif::ArtifactLocationBuilder::default() 114 | .uri(self.path.clone()) 115 | .build() 116 | .unwrap(), 117 | ) 118 | .replacements( 119 | replacements 120 | .iter() 121 | .map(|replacement| { 122 | sarif::ReplacementBuilder::default() 123 | .deleted_region( 124 | sarif::RegionBuilder::default() 125 | .start_line( 126 | replacement.deleted_region.start.line as i64 + 1, 127 | ) 128 | .start_column( 129 | replacement.deleted_region.start.character as i64 + 1, 130 | ) 131 | .end_line(replacement.deleted_region.end.line as i64 + 1) 132 | .end_column( 133 | replacement.deleted_region.end.character as i64 + 1, 134 | ) 135 | .build() 136 | .unwrap(), 137 | ) 138 | .inserted_content( 139 | sarif::ArtifactContentBuilder::default() 140 | .text(replacement.inserted_content.clone()) 141 | .build() 142 | .unwrap(), 143 | ) 144 | .build() 145 | .unwrap() 146 | }) 147 | .collect::>(), 148 | ) 149 | .build() 150 | .unwrap()]) 151 | .build() 152 | .unwrap() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, FixedOffset}; 2 | use git2::{AttrCheckFlags, AttrValue, Delta, DiffOptions, Repository}; 3 | use lazy_static::lazy_static; 4 | use std::collections::HashMap; 5 | use std::path::{Path, PathBuf}; 6 | use std::process::Command; 7 | use std::process::Output as ProcessOutput; 8 | use std::sync::Mutex; 9 | #[derive(Debug, Clone)] 10 | pub struct Hunk { 11 | pub path: PathBuf, 12 | 13 | /// 1-indexed line number, inclusive 14 | pub begin: u64, 15 | 16 | /// 1-indexed line number, exclusive 17 | pub end: u64, 18 | } 19 | 20 | #[derive(Debug, PartialEq, Eq, Hash)] 21 | pub enum FileStatus { 22 | Added, 23 | Modified, 24 | Deleted, 25 | } 26 | 27 | #[derive(Debug, Default)] 28 | pub struct FileChanges { 29 | /// Set of modified line ranges in new/existing files 30 | pub hunks: Vec, 31 | 32 | /// Map of changed files and FileStatus 33 | pub paths: HashMap, 34 | } 35 | 36 | pub struct Output { 37 | pub status: std::process::ExitStatus, 38 | pub stdout: String, 39 | pub stderr: String, 40 | } 41 | 42 | pub struct Commit { 43 | pub hash: String, 44 | pub date: DateTime, 45 | } 46 | 47 | impl Output { 48 | pub fn new(po: ProcessOutput) -> Self { 49 | Self { 50 | status: po.status, 51 | stdout: String::from_utf8_lossy(&po.stdout).to_string(), 52 | stderr: String::from_utf8_lossy(&po.stderr).to_string(), 53 | } 54 | } 55 | } 56 | lazy_static! { 57 | static ref LFS_CACHE: Mutex> = Mutex::new(HashMap::new()); 58 | } 59 | 60 | fn is_lfs(repo: &Repository, path: &Path) -> bool { 61 | let path_str = path.to_string_lossy().to_string(); 62 | 63 | // Check the cache first 64 | if let Some(&cached_result) = LFS_CACHE.lock().unwrap().get(&path_str) { 65 | return cached_result; 66 | } 67 | 68 | // "filter" is the primary LFS attribute, see gitattributes(5) 69 | // FILE_THEN_INDEX checks working tree then index; mimics git itself 70 | // https://github.com/libgit2/libgit2/blob/v1.5.0/include/git2/attr.h#L104-L116 71 | let result = if let Ok(filter_bytes) = 72 | repo.get_attr_bytes(path, "filter", AttrCheckFlags::FILE_THEN_INDEX) 73 | { 74 | let filter = AttrValue::from_bytes(filter_bytes); 75 | filter.eq(&AttrValue::from_string(Some("lfs"))) 76 | } else { 77 | false 78 | }; 79 | 80 | // Store the result in the cache 81 | LFS_CACHE.lock().unwrap().insert(path_str, result); 82 | 83 | result 84 | } 85 | 86 | pub fn modified_since(upstream: &str, repo_path: Option<&Path>) -> anyhow::Result { 87 | let path = repo_path.unwrap_or(Path::new(".")); 88 | let repo = Repository::open(path)?; 89 | 90 | let upstream_tree = match repo.find_reference(upstream) { 91 | Ok(reference) => reference.peel_to_tree()?, 92 | _ => repo.revparse_single(upstream)?.peel_to_tree()?, 93 | }; 94 | 95 | let diff = { 96 | let mut diff_opts = DiffOptions::new(); 97 | diff_opts.include_untracked(true); 98 | 99 | repo.diff_tree_to_workdir_with_index(Some(&upstream_tree), Some(&mut diff_opts))? 100 | }; 101 | 102 | // Iterate through the git diff, building hunks that match the new or modified lines in the 103 | // diff between the upstream and the working directory. Algorithm is as follows: 104 | // 105 | // current_hunk = None 106 | // for (delta, hunk, line) in diff: 107 | // if old_lineno == 0, new_lineno == 0: 108 | // impossible; do nothing 109 | // if old_lineno nonzero, new_lineno == 0: 110 | // deleted line; do nothing 111 | // if old_lineno == 0, new_lineno nonzero: 112 | // new or modified line; create or append to current hunk 113 | // if old_lineno nonzero, new_lineno nonzero: 114 | // context line or moved line; terminate current hunk 115 | // 116 | // The reason we have to do this re-hunking is because if the line numbers of an ICTC block 117 | // change - likely because more lines were added to the file preceding it - libgit2 will create 118 | // a DiffHunk which includes the moved lines, so we can't just create one hunk per DiffHunk. 119 | // Instead, we have to break up DiffHunk instances in up to N hunks, since we only care about 120 | // the new/modified section of the diff. 121 | // 122 | // See https://docs.rs/git2/latest/git2/struct.Diff.html#method.foreach and the underlying API 123 | // docs at https://libgit2.org/libgit2/#HEAD/group/diff/git_diff_foreach. 124 | let mut ret = FileChanges::default(); 125 | let mut maybe_current_hunk: Option = None; 126 | diff.foreach( 127 | &mut |delta: git2::DiffDelta<'_>, _| { 128 | if let Some(path) = delta.new_file().path() { 129 | if !is_lfs(&repo, path) { 130 | match delta.status() { 131 | Delta::Added => { 132 | ret.paths 133 | .insert(path.to_string_lossy().to_string(), FileStatus::Added); 134 | } 135 | Delta::Modified => { 136 | ret.paths 137 | .insert(path.to_string_lossy().to_string(), FileStatus::Modified); 138 | } 139 | Delta::Deleted => { 140 | ret.paths 141 | .insert(path.to_string_lossy().to_string(), FileStatus::Deleted); 142 | } 143 | _ => {} 144 | } 145 | } 146 | } 147 | true 148 | }, 149 | None, 150 | None, 151 | Some(&mut |delta, _, line| { 152 | if let Some(path) = delta.new_file().path() { 153 | match delta.status() { 154 | Delta::Added 155 | | Delta::Copied 156 | | Delta::Untracked 157 | | Delta::Modified 158 | | Delta::Renamed => { 159 | if !is_lfs(&repo, path) { 160 | if let Some(new_lineno) = line.new_lineno() { 161 | if line.old_lineno().is_none() { 162 | maybe_current_hunk = maybe_current_hunk 163 | .as_ref() 164 | .map(|current_hunk| Hunk { 165 | path: current_hunk.path.clone(), 166 | begin: current_hunk.begin, 167 | end: (new_lineno as u64) + 1, 168 | }) 169 | .or_else(|| { 170 | Some(Hunk { 171 | path: path.to_path_buf(), 172 | begin: new_lineno as u64, 173 | end: (new_lineno as u64) + 1, 174 | }) 175 | }); 176 | } else if let Some(current_hunk) = &maybe_current_hunk { 177 | ret.hunks.push(current_hunk.clone()); 178 | maybe_current_hunk = None; 179 | } 180 | } 181 | } 182 | } 183 | Delta::Unmodified 184 | | Delta::Deleted 185 | | Delta::Ignored 186 | | Delta::Typechange 187 | | Delta::Unreadable 188 | | Delta::Conflicted => (), 189 | } 190 | } 191 | true 192 | }), 193 | )?; 194 | 195 | if let Some(current_hunk) = &maybe_current_hunk { 196 | ret.hunks.push(current_hunk.clone()); 197 | } 198 | 199 | Ok(ret) 200 | } 201 | 202 | pub fn clone(repo_url: &str, destination: &Path) -> Output { 203 | let output = Command::new("git") 204 | .args([ 205 | "clone", 206 | "--no-checkout", 207 | "--bare", 208 | "--filter=blob:none", 209 | repo_url, 210 | destination.to_string_lossy().as_ref(), 211 | ]) 212 | .output() 213 | .expect("Failed to execute git command"); 214 | 215 | Output::new(output) 216 | } 217 | 218 | pub fn status(dir: &PathBuf) -> Output { 219 | let output = Command::new("git") 220 | .args(["status", "--porcelain"]) 221 | .current_dir(dir) 222 | .output() 223 | .expect("Failed to execute git command"); 224 | 225 | Output::new(output) 226 | } 227 | 228 | pub fn dir_inside_git_repo(dir: &PathBuf) -> bool { 229 | let output = Command::new("git") 230 | .args(["rev-parse", "--is-inside-work-tree"]) 231 | .current_dir(dir) 232 | .output() 233 | .expect("Failed to execute git command"); 234 | 235 | output.status.success() 236 | } 237 | 238 | pub fn last_commit(dir: &PathBuf, file: &str) -> Result { 239 | let result = Command::new("git") 240 | .args([ 241 | "--no-pager", 242 | "log", 243 | "-1", 244 | "--pretty=format:%H%n%ci", 245 | "--", 246 | file, 247 | ]) 248 | .current_dir(dir) 249 | .output() 250 | .expect("Failed to execute git command"); 251 | 252 | let output = Output::new(result); 253 | 254 | if output.status.success() { 255 | if output.stdout.is_empty() { 256 | return Err("No file history found".to_string()); 257 | } else { 258 | let mut lines: std::str::Lines<'_> = output.stdout.lines(); 259 | let hash = lines.next().ok_or("Missing hash").unwrap(); 260 | let date_str = lines.next().ok_or("Missing date").unwrap(); 261 | 262 | return Ok(Commit { 263 | hash: hash.to_string(), 264 | date: DateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S %z").unwrap(), 265 | }); 266 | } 267 | } 268 | Err(output.stderr) 269 | } 270 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod diagnostic; 3 | pub mod git; 4 | pub mod rules; 5 | pub mod run; 6 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use confique::Config; 3 | use horton::config::Conf; 4 | use horton::diagnostic; 5 | use horton::rules::if_change_then_change::ictc; 6 | use horton::rules::never_edit::never_edit; 7 | use horton::rules::no_curly_quotes::no_curly_quotes; 8 | use horton::rules::pls_no_land::pls_no_land; 9 | use horton::run::{Cli, OutputFormat, Run, Subcommands}; 10 | 11 | use log::{debug, warn}; 12 | use serde_sarif::sarif; 13 | use std::env; 14 | use std::path::{Path, PathBuf}; 15 | use std::time::Instant; 16 | 17 | use log::LevelFilter; 18 | use log4rs::{ 19 | append::console::ConsoleAppender, 20 | config::{Appender, Root}, 21 | encode::pattern::PatternEncoder, 22 | }; 23 | 24 | fn generate_line_string(original_results: &diagnostic::Diagnostics) -> String { 25 | return original_results 26 | .diagnostics 27 | .iter() 28 | .map(|d| { 29 | if let Some(range) = &d.range { 30 | format!( 31 | "{}:{}:{}: {} ({})", 32 | d.path, range.start.line, range.start.character, d.message, d.severity 33 | ) 34 | } else { 35 | format!("{}: {} ({})", d.path, d.message, d.severity) 36 | } 37 | }) 38 | .collect::>() 39 | .join("\n"); 40 | } 41 | 42 | fn generate_sarif_string( 43 | original_results: &diagnostic::Diagnostics, 44 | run_context: &Run, 45 | start_time: &Instant, 46 | ) -> anyhow::Result { 47 | // TODO(sam): figure out how to stop using unwrap() inside the map() calls below 48 | let mut results: Vec = original_results 49 | .diagnostics 50 | .iter() 51 | .map(|d| d.to_sarif()) 52 | .collect(); 53 | 54 | let r = sarif::ResultBuilder::default() 55 | .message( 56 | sarif::MessageBuilder::default() 57 | .text(format!( 58 | "{:?} files processed in {:?} files:[{}]", 59 | run_context.paths.len(), 60 | start_time.elapsed(), 61 | run_context 62 | .paths 63 | .iter() 64 | .map(|p| p.to_string_lossy()) 65 | .collect::>() 66 | .join(", ") 67 | )) 68 | .build() 69 | .unwrap(), 70 | ) 71 | .rule_id("toolbox-perf") 72 | .level(diagnostic::Severity::Note.to_string()) 73 | .build() 74 | .unwrap(); 75 | 76 | results.push(r); 77 | 78 | let run = sarif::RunBuilder::default() 79 | .tool( 80 | sarif::ToolBuilder::default() 81 | .driver( 82 | sarif::ToolComponentBuilder::default() 83 | .name("trunk-toolbox") 84 | .build() 85 | .unwrap(), 86 | ) 87 | .build() 88 | .unwrap(), 89 | ) 90 | .results(results) 91 | .build()?; 92 | let sarif_built = sarif::SarifBuilder::default() 93 | .version("2.1.0") 94 | .runs([run]) 95 | .build()?; 96 | 97 | let sarif = serde_json::to_string_pretty(&sarif_built)?; 98 | Ok(sarif) 99 | } 100 | 101 | fn find_toolbox_toml() -> Option { 102 | let files = ["toolbox.toml", ".config/toolbox.toml"]; 103 | 104 | for file in &files { 105 | if Path::new(file).exists() { 106 | return Some(file.to_string()); 107 | } 108 | } 109 | 110 | None 111 | } 112 | 113 | fn run() -> anyhow::Result<()> { 114 | let start = Instant::now(); 115 | let cli: Cli = Cli::parse(); 116 | 117 | if let Some(Subcommands::Genconfig {}) = &cli.subcommand { 118 | Conf::print_default(); 119 | return Ok(()); 120 | } 121 | 122 | let mut ret = diagnostic::Diagnostics::default(); 123 | 124 | // If not configuration file is provided the default config will be used 125 | // some parts of toolbo can run with the default config 126 | let toolbox_toml: String = match find_toolbox_toml() { 127 | Some(file) => file, 128 | None => "no_config_found.toml".to_string(), 129 | }; 130 | 131 | let config = Conf::builder() 132 | .env() 133 | .file(&toolbox_toml) 134 | .load() 135 | .unwrap_or_else(|err| { 136 | eprintln!("Toolbox cannot run: {}", err); 137 | std::process::exit(1); 138 | }); 139 | 140 | let run = Run { 141 | paths: cli.files.into_iter().map(PathBuf::from).collect(), 142 | config, 143 | config_path: toolbox_toml, 144 | cache_dir: cli.cache_dir.clone(), 145 | }; 146 | 147 | let (pls_no_land_result, ictc_result): (Result<_, _>, Result<_, _>) = 148 | rayon::join(|| pls_no_land(&run), || ictc(&run, &cli.upstream)); 149 | 150 | match pls_no_land_result { 151 | Ok(result) => ret.diagnostics.extend(result), 152 | Err(e) => return Err(e), 153 | } 154 | 155 | match ictc_result { 156 | Ok(result) => ret.diagnostics.extend(result), 157 | Err(e) => return Err(e), 158 | } 159 | 160 | //TODO: refactor this to use a threadpool for all the rules. using rayon::join() won't scale 161 | //beyond two things 162 | let ne_result = never_edit(&run, &cli.upstream); 163 | match ne_result { 164 | Ok(result) => ret.diagnostics.extend(result), 165 | Err(e) => return Err(e), 166 | } 167 | 168 | let ncq_result = no_curly_quotes(&run, &cli.upstream); 169 | match ncq_result { 170 | Ok(result) => ret.diagnostics.extend(result), 171 | Err(e) => return Err(e), 172 | } 173 | 174 | let mut output_string = generate_line_string(&ret); 175 | if cli.output_format == OutputFormat::Sarif { 176 | output_string = generate_sarif_string(&ret, &run, &start)?; 177 | } 178 | 179 | if let Some(outfile) = &cli.results { 180 | std::fs::write(outfile, output_string)?; 181 | } else { 182 | println!("{}", output_string); 183 | } 184 | 185 | Ok(()) 186 | } 187 | 188 | fn init_default_logger() { 189 | // Create a console appender for stdout 190 | let stdout = ConsoleAppender::builder() 191 | .encoder(Box::new(PatternEncoder::new( 192 | "{d(%H:%M:%S)} | {({l}):5.5} | {f}:{L} | {m}{n}", 193 | ))) 194 | .build(); 195 | 196 | // Build the log4rs configuration - log only errors to stdout by default 197 | let config = log4rs::Config::builder() 198 | .appender(Appender::builder().build("stdout", Box::new(stdout))) 199 | .build(Root::builder().appender("stdout").build(LevelFilter::Error)) 200 | .expect("Failed to build log4rs configuration"); 201 | 202 | log4rs::init_config(config).unwrap(); 203 | } 204 | 205 | fn main() { 206 | // initialize logging from file if log4rs.yaml exists 207 | let current_dir = env::current_dir().expect("Failed to get current directory"); 208 | let log_config_path = current_dir.join("log4rs.yaml"); 209 | if log_config_path.exists() { 210 | match log4rs::init_file(&log_config_path, Default::default()) { 211 | Ok(_) => { 212 | // Initialization succeeded 213 | debug!("logging initialized - {:?}", log_config_path); 214 | } 215 | Err(e) => { 216 | init_default_logger(); 217 | warn!("Falling back to default logging setup. override with valid 'log4rs.yaml' file, {}", e); 218 | } 219 | } 220 | } else { 221 | init_default_logger(); 222 | debug!("using default built-in logging setup - no log4rs.yaml found"); 223 | } 224 | 225 | match run() { 226 | Ok(_) => (), 227 | Err(err) => { 228 | log::error!("{}", err); 229 | std::process::exit(1); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/rules/if_change_then_change.rs: -------------------------------------------------------------------------------- 1 | use crate::run::Run; 2 | use anyhow::Context; 3 | use log::{debug, trace, warn}; 4 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 5 | use regex::Regex; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::fs::File; 8 | use std::io::{BufRead, BufReader}; 9 | use std::path::Path; 10 | use std::path::PathBuf; 11 | 12 | use crate::diagnostic; 13 | use crate::git; 14 | 15 | #[derive(Debug)] 16 | pub enum ThenChange { 17 | RemoteFile(String), 18 | RepoFile(PathBuf), 19 | MissingIf, 20 | MissingThen, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct IctcBlock { 25 | pub path: PathBuf, 26 | pub begin: Option, 27 | pub end: Option, 28 | pub thenchange: Option, 29 | } 30 | 31 | impl IctcBlock { 32 | fn get_range(&self) -> diagnostic::Range { 33 | diagnostic::Range { 34 | start: diagnostic::Position { 35 | line: self.begin.unwrap(), 36 | character: 0, 37 | }, 38 | end: diagnostic::Position { 39 | line: self.end.unwrap(), 40 | character: 0, 41 | }, 42 | } 43 | } 44 | } 45 | 46 | lazy_static::lazy_static! { 47 | static ref RE_BEGIN: Regex = Regex::new(r"(?i)^\s*(//|#)\s*ifchange(.*)$").unwrap(); 48 | static ref RE_END: Regex = Regex::new(r"(?i)^\s*(//|#)\s*thenchange(.*)$").unwrap(); 49 | } 50 | 51 | pub fn find_ictc_blocks(path: &PathBuf) -> anyhow::Result> { 52 | let mut blocks: Vec = Vec::new(); 53 | 54 | trace!("scanning contents of {}", path.display()); 55 | 56 | let in_file = File::open(path).with_context(|| { 57 | let error_message = format!("failed to open: {:#?}", path); 58 | warn!("{}", error_message); 59 | error_message 60 | })?; 61 | 62 | let in_buf = BufReader::new(in_file); 63 | 64 | let mut block: Option = None; 65 | 66 | for (i, line) in lines_view(in_buf) 67 | .context(format!("failed to read lines of text from: {:#?}", path))? 68 | .iter() 69 | .enumerate() 70 | .map(|(i, line)| (i + 1, line)) 71 | { 72 | let line_no = Some(i as u64); 73 | if RE_BEGIN.find(line).is_some() { 74 | if let Some(mut block_value) = block { 75 | // Two if blocks in a row - report problem 76 | block_value.end = block_value.begin; 77 | block_value.thenchange = Some(ThenChange::MissingThen); 78 | blocks.push(block_value); 79 | } 80 | 81 | block = Some(IctcBlock { 82 | path: path.clone(), 83 | begin: line_no, 84 | end: None, 85 | thenchange: None, 86 | }); 87 | } else if let Some(end_capture) = RE_END.captures(line) { 88 | if let Some(mut block_value) = block { 89 | block_value.end = line_no; 90 | block_value.thenchange = Some(ThenChange::RepoFile(PathBuf::from( 91 | end_capture 92 | .get(2) 93 | .with_context(|| "expected at least 3 captures")? 94 | .as_str() 95 | .trim(), 96 | ))); 97 | blocks.push(block_value); 98 | block = None; 99 | } else { 100 | // block is None and we found a IfChange without a ThenChange 101 | blocks.push(IctcBlock { 102 | path: path.clone(), 103 | begin: line_no, 104 | end: line_no, 105 | thenchange: Some(ThenChange::MissingIf), 106 | }); 107 | } 108 | } 109 | } 110 | 111 | // If we have an unclosed block - record that 112 | if let Some(mut block_value) = block { 113 | block_value.end = block_value.begin; 114 | block_value.thenchange = Some(ThenChange::MissingThen); 115 | blocks.push(block_value); 116 | } 117 | 118 | Ok(blocks) 119 | } 120 | 121 | pub fn ictc(run: &Run, upstream: &str) -> anyhow::Result> { 122 | let config = &run.config.ifchange; 123 | 124 | if !config.enabled { 125 | trace!("'ifchange' is disabled"); 126 | return Ok(vec![]); 127 | } 128 | 129 | if run.is_upstream() { 130 | trace!("'if-change' rule doesn't run on upstream"); 131 | return Ok(vec![]); 132 | } 133 | 134 | debug!( 135 | "scanning {} files for if_change_then_change", 136 | run.paths.len() 137 | ); 138 | 139 | // Build up list of files that actually have a ifchange block - this way we can avoid 140 | // processing git modified chunks if none are present 141 | let all_blocks: Vec<_> = run 142 | .paths 143 | .par_iter() 144 | .filter_map(|file| find_ictc_blocks(file).ok()) 145 | .flatten() 146 | .collect(); 147 | 148 | // Fast exit if we don't have any files that have ICTC blocks - saves us calling 149 | // into git to get more information 150 | if all_blocks.is_empty() { 151 | return Ok(vec![]); 152 | } 153 | 154 | let modified = git::modified_since(upstream, None)?; 155 | let hunks = &modified.hunks; 156 | 157 | log::trace!("modified stats, per libgit2:\n{:#?}", modified); 158 | 159 | // TODO(sam): this _should_ be a iter-map-collect, but unclear how to apply a reducer 160 | // between the map and collect (there can be multiple hunks with the same path) 161 | let mut modified_lines_by_path: HashMap> = HashMap::new(); 162 | for h in hunks { 163 | modified_lines_by_path 164 | .entry(h.path.clone()) 165 | .or_default() 166 | .extend(h.begin..h.end); 167 | } 168 | let modified_lines_by_path = modified_lines_by_path; 169 | 170 | let mut blocks: Vec = Vec::new(); 171 | 172 | for block in all_blocks { 173 | if let Some(thenchange) = &block.thenchange { 174 | match &thenchange { 175 | ThenChange::MissingIf | ThenChange::MissingThen => { 176 | blocks.push(block); 177 | } 178 | _ => { 179 | if let (Some(begin), Some(end)) = (block.begin, block.end) { 180 | let block_lines = HashSet::from_iter(begin..end); 181 | if !block_lines.is_disjoint( 182 | modified_lines_by_path 183 | .get(&block.path) 184 | .unwrap_or(&HashSet::new()), 185 | ) { 186 | blocks.push(block); 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | let blocks_by_path: HashMap<&PathBuf, &IctcBlock> = 195 | blocks.iter().map(|b| (&b.path, b)).collect(); 196 | 197 | let mut diagnostics: Vec = Vec::new(); 198 | 199 | for block in &blocks { 200 | if let Some(change) = &block.thenchange { 201 | match change { 202 | ThenChange::RemoteFile(_remote_file) => { 203 | todo!("build support for remote file") 204 | } 205 | ThenChange::RepoFile(local_file) => { 206 | // Check if the repo file exists - if it was deleted this is a warning 207 | if !Path::new(local_file).exists() { 208 | diagnostics.push(diagnostic::Diagnostic { 209 | path: block.path.to_str().unwrap().to_string(), 210 | range: Some(block.get_range()), 211 | severity: diagnostic::Severity::Warning, 212 | code: "if-change-file-does-not-exist".to_string(), 213 | message: format!("ThenChange {} does not exist", local_file.display(),), 214 | replacements: None, 215 | }); 216 | } 217 | // If target file was not changed raise issue 218 | if blocks_by_path.get(&local_file).is_none() { 219 | diagnostics.push(diagnostic::Diagnostic { 220 | path: block.path.to_str().unwrap().to_string(), 221 | range: Some(block.get_range()), 222 | severity: diagnostic::Severity::Error, 223 | code: "if-change-then-change-this".to_string(), 224 | message: format!( 225 | "Expected change in {} because {} was modified", 226 | local_file.display(), 227 | block.path.display(), 228 | ), 229 | replacements: None, 230 | }); 231 | } 232 | } 233 | ThenChange::MissingIf => { 234 | diagnostics.push(diagnostic::Diagnostic { 235 | path: block.path.to_str().unwrap().to_string(), 236 | range: Some(block.get_range()), 237 | severity: diagnostic::Severity::Warning, 238 | code: "if-change-mismatched".to_string(), 239 | message: "Expected preceding IfChange tag".to_string(), 240 | replacements: None, 241 | }); 242 | } 243 | ThenChange::MissingThen => { 244 | diagnostics.push(diagnostic::Diagnostic { 245 | path: block.path.to_str().unwrap().to_string(), 246 | range: Some(block.get_range()), 247 | severity: diagnostic::Severity::Warning, 248 | code: "if-change-mismatched".to_string(), 249 | message: "Expected matching ThenChange tag".to_string(), 250 | replacements: None, 251 | }); 252 | } 253 | } 254 | } 255 | } 256 | 257 | trace!("ICTC blocks are:\n{:?}", blocks); 258 | 259 | Ok(diagnostics) 260 | } 261 | 262 | type LinesView = Vec; 263 | 264 | fn lines_view(reader: R) -> anyhow::Result { 265 | let mut ret: LinesView = LinesView::default(); 266 | for line in reader.lines() { 267 | let line = line?; 268 | ret.push(line); 269 | } 270 | Ok(ret) 271 | } 272 | -------------------------------------------------------------------------------- /src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod if_change_then_change; 2 | pub mod never_edit; 3 | pub mod no_curly_quotes; 4 | pub mod pls_no_land; 5 | -------------------------------------------------------------------------------- /src/rules/never_edit.rs: -------------------------------------------------------------------------------- 1 | use crate::config::NeverEditConf; 2 | use crate::git::FileStatus; 3 | use crate::run::Run; 4 | use glob::glob; 5 | use glob_match::glob_match; 6 | 7 | use log::debug; 8 | use log::trace; 9 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 10 | 11 | use crate::diagnostic; 12 | use crate::git; 13 | 14 | pub fn is_never_edit(file_path: &str, config: &NeverEditConf) -> bool { 15 | for glob_path in &config.paths { 16 | if glob_match(glob_path, file_path) { 17 | log::info!("matched: {} with {}", glob_path, file_path); 18 | return true; 19 | } 20 | } 21 | false 22 | } 23 | 24 | pub fn never_edit(run: &Run, upstream: &str) -> anyhow::Result> { 25 | let config = &run.config.neveredit; 26 | 27 | if !config.enabled { 28 | trace!("'neveredit' is disabled"); 29 | return Ok(vec![]); 30 | } 31 | 32 | let mut diagnostics: Vec = Vec::new(); 33 | 34 | // We only emit config issues for the current run (not the upstream) so we can guarantee 35 | // that config issues get reported and not conceiled by HTL 36 | if config.paths.is_empty() && !run.is_upstream() { 37 | trace!("'neveredit' no protected paths configured"); 38 | diagnostics.push(diagnostic::Diagnostic { 39 | path: run.config_path.clone(), 40 | range: None, 41 | severity: diagnostic::Severity::Warning, 42 | code: "never-edit-config".to_string(), 43 | message: "no protected paths provided in config".to_string(), 44 | replacements: None, 45 | }); 46 | return Ok(diagnostics); 47 | } 48 | 49 | // We only report diagnostic issues for config when not running as upstream 50 | if !run.is_upstream() { 51 | debug!("verifying protected paths are valid and exist"); 52 | for glob_path in &config.paths { 53 | let mut matches_something = false; 54 | match glob(glob_path) { 55 | Ok(paths) => { 56 | for entry in paths { 57 | match entry { 58 | Ok(_path) => { 59 | matches_something = true; 60 | break; 61 | } 62 | Err(e) => println!("Error reading path: {:?}", e), 63 | } 64 | } 65 | if !matches_something { 66 | diagnostics.push(diagnostic::Diagnostic { 67 | path: run.config_path.clone(), 68 | range: None, 69 | severity: diagnostic::Severity::Warning, 70 | code: "never-edit-bad-config".to_string(), 71 | message: format!("{:?} does not protect any existing files", glob_path), 72 | replacements: None, 73 | }); 74 | } 75 | } 76 | Err(_e) => { 77 | diagnostics.push(diagnostic::Diagnostic { 78 | path: run.config_path.clone(), 79 | range: None, 80 | severity: diagnostic::Severity::Warning, 81 | code: "never-edit-bad-config".to_string(), 82 | message: format!("{:?} is not a valid glob pattern", glob_path), 83 | replacements: None, 84 | }); 85 | } 86 | } 87 | } 88 | } 89 | 90 | // Build up list of files that are being checked and are protected 91 | let protected_files: Vec<_> = run 92 | .paths 93 | .par_iter() 94 | .filter_map(|file| { 95 | file.to_str().and_then(|file_str| { 96 | if is_never_edit(file_str, config) { 97 | Some(file_str.to_string()) 98 | } else { 99 | None 100 | } 101 | }) 102 | }) 103 | .collect(); 104 | 105 | // Fast exit if we don't have any files changed that are protected 106 | if protected_files.is_empty() { 107 | return Ok(diagnostics); 108 | } 109 | 110 | debug!( 111 | "tool configured for {} protected files", 112 | protected_files.len() 113 | ); 114 | 115 | let modified = git::modified_since(upstream, None)?; 116 | 117 | for protected_file in &protected_files { 118 | if let Some(status) = modified.paths.get(protected_file) { 119 | match status { 120 | FileStatus::Modified => { 121 | diagnostics.push(diagnostic::Diagnostic { 122 | path: protected_file.clone(), 123 | range: None, 124 | severity: diagnostic::Severity::Error, 125 | code: "never-edit-modified".to_string(), 126 | message: "file is protected and should not be modified".to_string(), 127 | replacements: None, 128 | }); 129 | } 130 | FileStatus::Deleted => { 131 | diagnostics.push(diagnostic::Diagnostic { 132 | path: protected_file.clone(), 133 | range: None, 134 | severity: diagnostic::Severity::Warning, 135 | code: "never-edit-deleted".to_string(), 136 | message: "file is protected and should not be deleted".to_string(), 137 | replacements: None, 138 | }); 139 | } 140 | _ => {} 141 | } 142 | } 143 | } 144 | 145 | diagnostics.push(diagnostic::Diagnostic { 146 | path: "".to_string(), 147 | range: None, 148 | severity: diagnostic::Severity::Note, 149 | code: "toolbox-never-edit-perf".to_string(), 150 | message: format!("{:?} protected files checked", protected_files.len()), 151 | replacements: None, 152 | }); 153 | 154 | Ok(diagnostics) 155 | } 156 | -------------------------------------------------------------------------------- /src/rules/no_curly_quotes.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::BufRead; 3 | use std::io::BufReader; 4 | use std::path::PathBuf; 5 | 6 | use crate::run::Run; 7 | 8 | use anyhow::Context; 9 | use log::debug; 10 | use log::trace; 11 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 12 | 13 | use crate::diagnostic::{Diagnostic, Position, Range, Replacement, Severity}; 14 | 15 | pub fn no_curly_quotes(run: &Run, _upstream: &str) -> anyhow::Result> { 16 | let config = &run.config.nocurlyquotes; 17 | 18 | if !config.enabled { 19 | trace!("'nocurlyquotes' is disabled"); 20 | return Ok(vec![]); 21 | } 22 | 23 | debug!("scanning {} files for curly quotes", run.paths.len()); 24 | 25 | // Scan files in parallel 26 | let results: Result, _> = run.paths.par_iter().map(no_curly_quotes_impl).collect(); 27 | 28 | match results { 29 | Ok(v) => Ok(v.into_iter().flatten().collect()), 30 | Err(e) => Err(e), 31 | } 32 | } 33 | 34 | const DOUBLE_CURLY_QUOTES: [char; 4] = ['\u{201C}', '\u{201D}', '\u{201E}', '\u{201F}']; 35 | const SINGLE_CURLY_QUOTES: [char; 2] = ['\u{2018}', '\u{2019}']; 36 | 37 | fn no_curly_quotes_impl(path: &PathBuf) -> anyhow::Result> { 38 | let in_file = File::open(path).with_context(|| format!("failed to open: {:#?}", path))?; 39 | let in_buf = BufReader::new(in_file); 40 | 41 | trace!("scanning contents of {}", path.display()); 42 | 43 | let lines_view = in_buf 44 | .lines() 45 | .collect::>>() 46 | .with_context(|| format!("failed to read lines of text from {:#?}", path))?; 47 | 48 | let mut ret = Vec::new(); 49 | 50 | for (i, line) in lines_view.iter().enumerate() { 51 | let mut char_issues = Vec::new(); 52 | 53 | for (pos, c) in line.char_indices() { 54 | if SINGLE_CURLY_QUOTES.contains(&c) { 55 | let char_pos = line[..pos].chars().count() as u64; 56 | char_issues.push((char_pos, "'")); 57 | } 58 | if DOUBLE_CURLY_QUOTES.contains(&c) { 59 | let char_pos = line[..pos].chars().count() as u64; 60 | char_issues.push((char_pos, "\"")); 61 | } 62 | } 63 | 64 | if char_issues.is_empty() { 65 | continue; 66 | } 67 | 68 | // Build an array of replacements for each character in char_positions 69 | let replacements: Vec = char_issues 70 | .iter() 71 | .map(|&(char_pos, rchar)| Replacement { 72 | deleted_region: Range { 73 | start: Position { 74 | line: i as u64, 75 | character: char_pos, 76 | }, 77 | end: Position { 78 | line: i as u64, 79 | character: char_pos + 1, 80 | }, 81 | }, 82 | inserted_content: rchar.to_string(), 83 | }) 84 | .collect(); 85 | 86 | ret.push(Diagnostic { 87 | path: path.to_str().unwrap().to_string(), 88 | range: Some(Range { 89 | start: Position { 90 | line: i as u64, 91 | character: char_issues.first().unwrap().0, 92 | }, 93 | end: Position { 94 | line: i as u64, 95 | character: char_issues.last().unwrap().0 + 1, 96 | }, 97 | }), 98 | severity: Severity::Error, 99 | code: "no-curly-quotes".to_string(), 100 | message: format!("Found curly quote on line {}", i + 1), 101 | replacements: Some(replacements), 102 | }); 103 | } 104 | 105 | Ok(ret) 106 | } 107 | -------------------------------------------------------------------------------- /src/rules/pls_no_land.rs: -------------------------------------------------------------------------------- 1 | // trunk-ignore-all(trunk-toolbox/do-not-land,trunk-toolbox/todo) 2 | extern crate regex; 3 | 4 | use crate::diagnostic; 5 | use crate::run::Run; 6 | use anyhow::Context; 7 | use log::{debug, trace}; 8 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 9 | use regex::Regex; 10 | use std::fs::File; 11 | use std::io::Read; 12 | use std::io::{BufRead, BufReader}; 13 | use std::path::{Path, PathBuf}; 14 | 15 | lazy_static::lazy_static! { 16 | static ref DNL_RE: Regex = Regex::new(r"(?i)(DO[\s_-]*NOT[\s_-]*LAND)").unwrap(); 17 | static ref TODO_RE: Regex = Regex::new(r"(?i)(TODO|FIXME)(\W+.*)?$").unwrap(); 18 | } 19 | 20 | pub fn is_binary_file(path: &PathBuf) -> std::io::Result { 21 | let mut file = File::open(path)?; 22 | let mut buffer = [0; 4096]; 23 | let n = file.read(&mut buffer)?; 24 | Ok(buffer[..n].contains(&0)) 25 | } 26 | 27 | pub fn is_ignored_file(path: &Path) -> bool { 28 | // Filter out well known files that should have the word donotland in them (like toolbox.toml) 29 | path.file_name().map_or(false, |f| f == "toolbox.toml") 30 | } 31 | 32 | // Checks for $re and other forms thereof in source code 33 | // 34 | // Note that this is named "pls_no_land" to avoid causing DNL matches everywhere in trunk-toolbox. 35 | pub fn pls_no_land(run: &Run) -> anyhow::Result> { 36 | let dnl_config = &run.config.donotland; 37 | let todo_config = &run.config.todo; 38 | 39 | // Avoid opening the file if neither are enabled. 40 | if !dnl_config.enabled && !todo_config.enabled { 41 | trace!("'donotland' is disabled"); 42 | trace!("'todo' is disabled"); 43 | return Ok(vec![]); 44 | } 45 | 46 | debug!("scanning {} files for pls_no_land", run.paths.len()); 47 | 48 | // Scan files in parallel 49 | let results: Result, _> = run 50 | .paths 51 | .par_iter() 52 | .map(|path| pls_no_land_impl(path, run)) 53 | .collect(); 54 | 55 | match results { 56 | Ok(v) => Ok(v.into_iter().flatten().collect()), 57 | Err(e) => Err(e), 58 | } 59 | } 60 | 61 | fn pls_no_land_impl(path: &PathBuf, run: &Run) -> anyhow::Result> { 62 | let config: &crate::config::Conf = &run.config; 63 | 64 | if is_binary_file(path).unwrap_or(true) { 65 | debug!("ignoring binary file {}", path.display()); 66 | return Ok(vec![]); 67 | } 68 | 69 | if is_ignored_file(path) { 70 | debug!("ignoring ignored file {}", path.display()); 71 | return Ok(vec![]); 72 | } 73 | 74 | let in_file = File::open(path).with_context(|| format!("failed to open: {:#?}", path))?; 75 | let mut in_buf = BufReader::new(in_file); 76 | 77 | let mut first_line = vec![]; 78 | in_buf.read_until(b'\n', &mut first_line)?; 79 | 80 | if first_line.is_empty() { 81 | return Ok(vec![]); 82 | } 83 | 84 | trace!("scanning contents of {}", path.display()); 85 | 86 | let first_line_view = String::from_utf8(first_line) 87 | .with_context(|| format!("could not read first line of {:#?}", path))?; 88 | let lines_view = in_buf 89 | .lines() 90 | .collect::>>() 91 | .with_context(|| format!("failed to read lines of text from {:#?}", path))?; 92 | 93 | let mut ret = Vec::new(); 94 | 95 | for (i, line) in [first_line_view] 96 | .iter() 97 | .chain(lines_view.iter()) 98 | .enumerate() 99 | { 100 | // DO-NOT-LAND should always fire and not use HTL semantics. So only generate 101 | // those warnings on the current code and skip the upstream 102 | if !line.contains("trunk-ignore(|-begin|-end|-all)\\(trunk-toolbox/(do-not-land)\\)") 103 | && config.donotland.enabled 104 | && !run.is_upstream() 105 | { 106 | if let Some(m) = DNL_RE.find(line) { 107 | ret.push(diagnostic::Diagnostic { 108 | path: path.to_str().unwrap().to_string(), 109 | range: Some(diagnostic::Range { 110 | start: diagnostic::Position { 111 | line: i as u64, 112 | character: m.start() as u64, 113 | }, 114 | end: diagnostic::Position { 115 | line: i as u64, 116 | character: m.end() as u64, 117 | }, 118 | }), 119 | severity: diagnostic::Severity::Error, 120 | code: "do-not-land".to_string(), 121 | message: format!("Found '{}'", m.as_str()), 122 | replacements: None, 123 | }); 124 | } 125 | } 126 | if !line.contains("trunk-ignore(|-begin|-end|-all)\\(trunk-toolbox/(todo)\\)") 127 | && config.todo.enabled 128 | { 129 | if let Some(m) = TODO_RE.captures(line) { 130 | let token = &m[1]; 131 | ret.push(diagnostic::Diagnostic { 132 | path: path.to_str().unwrap().to_string(), 133 | range: Some(diagnostic::Range { 134 | start: diagnostic::Position { 135 | line: i as u64, 136 | character: m.get(1).unwrap().start() as u64, 137 | }, 138 | end: diagnostic::Position { 139 | line: i as u64, 140 | // Remove one since we also check for a nonalpha character after the token. 141 | character: m.get(1).unwrap().end() as u64, 142 | }, 143 | }), 144 | // Lower severity than DNL 145 | severity: diagnostic::Severity::Warning, 146 | code: "todo".to_string(), 147 | message: format!("Found '{}'", token), 148 | replacements: None, 149 | }); 150 | } 151 | } 152 | } 153 | 154 | Ok(ret) 155 | } 156 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Conf; 2 | use std::collections::HashSet; 3 | use std::path::PathBuf; 4 | 5 | use clap::builder::PossibleValue; 6 | use clap::{Parser, Subcommand, ValueEnum}; 7 | 8 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 9 | pub enum OutputFormat { 10 | Sarif, 11 | Text, 12 | } 13 | 14 | impl ValueEnum for OutputFormat { 15 | fn value_variants<'a>() -> &'a [Self] { 16 | &[Self::Sarif, Self::Text] 17 | } 18 | 19 | fn to_possible_value(&self) -> Option { 20 | Some(match self { 21 | Self::Sarif => PossibleValue::new("sarif"), 22 | Self::Text => PossibleValue::new("text"), 23 | }) 24 | } 25 | } 26 | 27 | #[derive(Parser, Debug)] 28 | #[clap(version = env!("CARGO_PKG_VERSION"), author = "Trunk Technologies Inc.")] 29 | pub struct Cli { 30 | #[command(subcommand)] 31 | pub subcommand: Option, 32 | 33 | pub files: Vec, 34 | 35 | #[clap(long)] 36 | #[arg(default_value_t = String::from("HEAD"))] 37 | pub upstream: String, 38 | 39 | #[clap(long, default_value = "sarif")] 40 | pub output_format: OutputFormat, 41 | 42 | #[clap(long, default_value = "")] 43 | /// optional cache directory location 44 | pub cache_dir: String, 45 | 46 | #[clap(long)] 47 | /// optional path to write results to 48 | pub results: Option, 49 | } 50 | 51 | #[derive(Subcommand, Debug)] 52 | pub enum Subcommands { 53 | // print default config for toolbox 54 | /// Generate default configuration content for toolbox 55 | Genconfig, 56 | } 57 | 58 | pub struct Run { 59 | pub paths: HashSet, 60 | pub config: Conf, 61 | pub config_path: String, 62 | pub cache_dir: String, 63 | } 64 | 65 | impl Run { 66 | pub fn is_upstream(&self) -> bool { 67 | self.cache_dir.ends_with("-upstream") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/do_not_land_test.rs: -------------------------------------------------------------------------------- 1 | // trunk-ignore-all(trunk-toolbox/do-not-land) 2 | use spectral::prelude::*; 3 | 4 | mod integration_testing; 5 | use integration_testing::TestRepo; 6 | 7 | #[test] 8 | fn basic() -> anyhow::Result<()> { 9 | let test_repo = TestRepo::make()?; 10 | 11 | test_repo.write( 12 | "alpha.foo", 13 | "lorem ipsum dolor\ndo-NOT-lAnD\nsit amet\n".as_bytes(), 14 | ); 15 | test_repo.git_add_all()?; 16 | let horton = test_repo.run_horton()?; 17 | 18 | assert_that(&horton.exit_code).contains_value(0); 19 | assert_that(&horton.has_result("do-not-land", "Found 'do-NOT-lAnD'", Some("alpha.foo"))) 20 | .is_true(); 21 | 22 | Ok(()) 23 | } 24 | 25 | #[test] 26 | fn binary_files_ignored() -> anyhow::Result<()> { 27 | let test_repo = TestRepo::make()?; 28 | 29 | test_repo.write("alpha.foo.binary", include_bytes!("trunk-logo.png")); 30 | test_repo.git_add_all()?; 31 | let horton = test_repo.run_horton()?; 32 | 33 | assert_that(&horton.runs()).has_length(1); 34 | assert_that(&horton.has_result_with_rule_id("do-not-land")).is_false(); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[test] 40 | fn honor_disabled_in_config() -> anyhow::Result<()> { 41 | let test_repo = TestRepo::make()?; 42 | test_repo.write("alpha.foo", "do-not-land\n".as_bytes()); 43 | test_repo.git_add_all()?; 44 | 45 | { 46 | let horton = test_repo.run_horton()?; 47 | assert_that(&horton.has_result_with_rule_id("do-not-land")).is_true(); 48 | } 49 | 50 | let config = r#" 51 | [donotland] 52 | enabled = false 53 | "#; 54 | 55 | // Now disable the rule 56 | test_repo.write("toolbox.toml", config.as_bytes()); 57 | { 58 | let horton = test_repo.run_horton().unwrap(); 59 | assert_that(&horton.has_result_with_rule_id("do-not-land")).is_false(); 60 | } 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /tests/foo.bar: -------------------------------------------------------------------------------- 1 | change this file -------------------------------------------------------------------------------- /tests/if_change_then_change/basic_ictc.file: -------------------------------------------------------------------------------- 1 | Lorem ipsum odor amet, consectetuer adipiscing elit. Ornare elit nostra sit metus arcu maximus conubia. Nullam nec elit justo aenean penatibus. Vestibulum cras euismod orci morbi phasellus luctus, sit tincidunt dictum. Hendrerit fringilla augue morbi vel pharetra per fames. Nostra elit mi auctor suspendisse congue. Aptent magnis molestie dui integer turpis quam accumsan? Aenean quis lobortis per tristique mi. 2 | 3 | Platea platea donec duis gravida dapibus. Sodales iaculis eu pharetra finibus magna orci condimentum. Habitant nascetur praesent lobortis etiam vestibulum sollicitudin magna. Augue fames laoreet rutrum vel leo donec hendrerit senectus. Senectus malesuada efficitur suscipit; platea ipsum placerat. Bibendum platea mattis nullam feugiat lorem nibh augue. Nullam aliquet libero non mollis ridiculus blandit. Nisl lacus porta justo eu enim euismod facilisi curabitur tortor. Donec augue nunc aliquam ullamcorper faucibus arcu imperdiet suspendisse. 4 | 5 | Fusce sem sem volutpat phasellus congue quisque ante libero dictumst. Aposuere urna cubilia mi vitae felis velit. Aenean netus habitant molestie blandit pellentesque laoreet. Et est sed; cubilia adipiscing morbi suspendisse ac etiam. Amet facilisi ultrices vulputate arcu, inceptos taciti arcu. Mi malesuada eget sagittis morbi nisl nisi nunc platea. Ac convallis conubia, a urna auctor consectetur eu proin purus. Sollicitudin mollis natoque molestie morbi vulputate nascetur ante posuere phasellus. Neque finibus vestibulum class porttitor nullam tempor massa dis fusce. Netus ornare facilisi nisi molestie tortor. 6 | // IfChange 7 | Mus facilisis scelerisque quam semper metus orci gravida interdum mattis. Primis semper aliquet blandit condimentum pretium mus. Fames finibus torquent interdum vitae eu. At condimentum a eros turpis mus eros. Diam at interdum euismod conubia dapibus finibus fames adipiscing? Laoreet ut vehicula metus risus eros mollis mauris habitant. Mi elit ante; ullamcorper faucibus dictumst felis ac mattis. Nascetur arcu varius libero convallis penatibus aenean nunc. Molestie velit magnis turpis potenti massa suspendisse. 8 | 9 | Mollis ad litora adipiscing orci mi curae, vitae ipsum auctor. Malesuada aliquam tincidunt senectus justo volutpat elit taciti. Tortor massa malesuada pulvinar; eleifend vivamus sem enim. Nostra iaculis elit fusce dui rutrum sit nulla convallis. Semper pulvinar tellus magnis sociosqu nunc primis aliquam aptent. Nulla lobortis ridiculus dui ante libero sed. Gravida ut eros massa; hendrerit iaculis nisl. Ex dapibus ornare primis duis bibendum; in et cubilia. 10 | // ThenChange foo.bar 11 | Feugiat conubia sem potenti nec sed elementum torquent. Turpis consequat orci scelerisque; nunc purus arcu hac eu. Dapibus arcu nascetur mi posuere dictum adipiscing. Sed ex lobortis commodo elit facilisis fusce. Varius senectus mattis est tortor; proin ex. Ridiculus curabitur mattis senectus euismod primis ac. Felis dictum suspendisse fringilla sit varius. Nunc risus nullam magna laoreet fusce hendrerit sed sollicitudin. Maecenas eu id mollis felis id suspendisse. Posuere nisl velit egestas porttitor lectus curae. 12 | 13 | Lacus eleifend euismod leo mauris blandit dapibus faucibus. Neque non lectus vel class dapibus per adipiscing pulvinar. Vehicula risus vulputate commodo tincidunt vivamus pretium. Ultricies lectus neque vestibulum primis augue justo vivamus. Ridiculus maximus id mollis facilisis fermentum nulla sapien; vitae montes. Etiam tempus conubia maecenas habitant nec. Nam platea turpis inceptos sapien tristique. Libero maximus porta hac donec scelerisque tempus. Scelerisque velit blandit per tempus ipsum. 14 | 15 | Posuere maximus himenaeos, ut aenean gravida auctor. Nulla ullamcorper fusce sodales; iaculis ac habitasse lacus. Turpis est augue gravida; gravida proin senectus mollis inceptos. Vulputate risus risus tempus class nam aenean erat. Mattis tempor commodo leo felis class natoque egestas imperdiet dis. Vestibulum tristique class posuere nibh iaculis. Sem rhoncus elit pellentesque facilisis auctor. Sodales rhoncus faucibus adipiscing phasellus magnis volutpat placerat magna. 16 | 17 | Eros laoreet porttitor porta etiam metus. Tempus eget integer cras sed non vitae hendrerit scelerisque. Pharetra pulvinar ex semper donec diam dolor. Curabitur vel vitae cubilia curabitur mollis nostra himenaeos. Habitasse volutpat fusce euismod enim bibendum magna. Ac eget bibendum semper lectus class duis gravida metus dictumst. Consequat erat praesent dis netus platea condimentum gravida. Ex platea ipsum ullamcorper duis egestas ullamcorper suscipit mauris vitae. 18 | 19 | Semper finibus enim praesent tempus malesuada at elementum. Sem congue fusce litora, iaculis viverra aenean curabitur semper. In lectus ut libero vehicula curabitur conubia sodales. Pellentesque magna diam ridiculus; netus ad accumsan convallis mattis. Erat euismod feugiat posuere diam hendrerit. Imperdiet himenaeos varius efficitur at nibh interdum. Eros feugiat convallis gravida sodales tincidunt rutrum semper blandit. Nostra varius condimentum per suspendisse, dignissim morbi placerat ridiculus donec. Libero parturient massa mi nascetur volutpat tristique nibh. 20 | 21 | Porttitor magna litora tortor nibh aliquam sit ipsum. Tempor mi leo dolor egestas feugiat risus ultrices? Sit eu tellus suspendisse lacus nulla magna cras fusce. Nulla et venenatis potenti nullam felis laoreet. Nulla magna tellus platea urna laoreet. Vitae venenatis litora, turpis ridiculus cursus facilisi. Dis augue taciti faucibus fermentum a montes cras. Gravida platea vivamus luctus penatibus conubia orci adipiscing aptent enim. Parturient ligula nam etiam quisque dolor non quisque fermentum. 22 | 23 | Odio nascetur suspendisse convallis semper tincidunt mi. Turpis aenean vehicula taciti gravida dapibus suscipit, netus mattis. Aenean diam sagittis elit; nisl ut commodo neque ornare nam. Donec metus cubilia neque per malesuada sit per hendrerit. Ridiculus hac risus lacinia pulvinar mattis? Eleifend torquent fermentum semper nibh duis amet facilisis porta. 24 | -------------------------------------------------------------------------------- /tests/if_change_then_change/multiple_ictc.file: -------------------------------------------------------------------------------- 1 | Lorem ipsum odor amet, consectetuer adipiscing elit. Ornare elit nostra sit metus arcu maximus conubia. Nullam nec elit justo aenean penatibus. Vestibulum cras euismod orci morbi phasellus luctus, sit tincidunt dictum. Hendrerit fringilla augue morbi vel pharetra per fames. Nostra elit mi auctor suspendisse congue. Aptent magnis molestie dui integer turpis quam accumsan? Aenean quis lobortis per tristique mi. 2 | 3 | Platea platea donec duis gravida dapibus. Sodales iaculis eu pharetra finibus magna orci condimentum. Habitant nascetur praesent lobortis etiam vestibulum sollicitudin magna. Augue fames laoreet rutrum vel leo donec hendrerit senectus. Senectus malesuada efficitur suscipit; platea ipsum placerat. Bibendum platea mattis nullam feugiat lorem nibh augue. Nullam aliquet libero non mollis ridiculus blandit. Nisl lacus porta justo eu enim euismod facilisi curabitur tortor. Donec augue nunc aliquam ullamcorper faucibus arcu imperdiet suspendisse. 4 | 5 | Fusce sem sem volutpat phasellus congue quisque ante libero dictumst. Aposuere urna cubilia mi vitae felis velit. Aenean netus habitant molestie blandit pellentesque laoreet. Et est sed; cubilia adipiscing morbi suspendisse ac etiam. Amet facilisi ultrices vulputate arcu, inceptos taciti arcu. Mi malesuada eget sagittis morbi nisl nisi nunc platea. Ac convallis conubia, a urna auctor consectetur eu proin purus. Sollicitudin mollis natoque molestie morbi vulputate nascetur ante posuere phasellus. Neque finibus vestibulum class porttitor nullam tempor massa dis fusce. Netus ornare facilisi nisi molestie tortor. 6 | // IfChange 7 | Mus facilisis scelerisque quam semper metus orci gravida interdum mattis. Primis semper aliquet blandit condimentum pretium mus. Fames finibus torquent interdum vitae eu. At condimentum a eros turpis mus eros. Diam at interdum euismod conubia dapibus finibus fames adipiscing? Laoreet ut vehicula metus risus eros mollis mauris habitant. Mi elit ante; ullamcorper faucibus dictumst felis ac mattis. Nascetur arcu varius libero convallis penatibus aenean nunc. Molestie velit magnis turpis potenti massa suspendisse. 8 | 9 | Mollis ad litora adipiscing orci mi curae, vitae ipsum auctor. Malesuada aliquam tincidunt senectus justo volutpat elit taciti. Tortor massa malesuada pulvinar; eleifend vivamus sem enim. Nostra iaculis elit fusce dui rutrum sit nulla convallis. Semper pulvinar tellus magnis sociosqu nunc primis aliquam aptent. Nulla lobortis ridiculus dui ante libero sed. Gravida ut eros massa; hendrerit iaculis nisl. Ex dapibus ornare primis duis bibendum; in et cubilia. 10 | // ThenChange foo.bar 11 | Feugiat conubia sem potenti nec sed elementum torquent. Turpis consequat orci scelerisque; nunc purus arcu hac eu. Dapibus arcu nascetur mi posuere dictum adipiscing. Sed ex lobortis commodo elit facilisis fusce. Varius senectus mattis est tortor; proin ex. Ridiculus curabitur mattis senectus euismod primis ac. Felis dictum suspendisse fringilla sit varius. Nunc risus nullam magna laoreet fusce hendrerit sed sollicitudin. Maecenas eu id mollis felis id suspendisse. Posuere nisl velit egestas porttitor lectus curae. 12 | 13 | Lacus eleifend euismod leo mauris blandit dapibus faucibus. Neque non lectus vel class dapibus per adipiscing pulvinar. Vehicula risus vulputate commodo tincidunt vivamus pretium. Ultricies lectus neque vestibulum primis augue justo vivamus. Ridiculus maximus id mollis facilisis fermentum nulla sapien; vitae montes. Etiam tempus conubia maecenas habitant nec. Nam platea turpis inceptos sapien tristique. Libero maximus porta hac donec scelerisque tempus. Scelerisque velit blandit per tempus ipsum. 14 | 15 | Posuere maximus himenaeos, ut aenean gravida auctor. Nulla ullamcorper fusce sodales; iaculis ac habitasse lacus. Turpis est augue gravida; gravida proin senectus mollis inceptos. Vulputate risus risus tempus class nam aenean erat. Mattis tempor commodo leo felis class natoque egestas imperdiet dis. Vestibulum tristique class posuere nibh iaculis. Sem rhoncus elit pellentesque facilisis auctor. Sodales rhoncus faucibus adipiscing phasellus magnis volutpat placerat magna. 16 | // IfChange 17 | Eros laoreet porttitor porta etiam metus. Tempus eget integer cras sed non vitae hendrerit scelerisque. Pharetra pulvinar ex semper donec diam dolor. Curabitur vel vitae cubilia curabitur mollis nostra himenaeos. Habitasse volutpat fusce euismod enim bibendum magna. Ac eget bibendum semper lectus class duis gravida metus dictumst. Consequat erat praesent dis netus platea condimentum gravida. Ex platea ipsum ullamcorper duis egestas ullamcorper suscipit mauris vitae. 18 | // ThenChange path/to/file/something.else 19 | Semper finibus enim praesent tempus malesuada at elementum. Sem congue fusce litora, iaculis viverra aenean curabitur semper. In lectus ut libero vehicula curabitur conubia sodales. Pellentesque magna diam ridiculus; netus ad accumsan convallis mattis. Erat euismod feugiat posuere diam hendrerit. Imperdiet himenaeos varius efficitur at nibh interdum. Eros feugiat convallis gravida sodales tincidunt rutrum semper blandit. Nostra varius condimentum per suspendisse, dignissim morbi placerat ridiculus donec. Libero parturient massa mi nascetur volutpat tristique nibh. 20 | 21 | Porttitor magna litora tortor nibh aliquam sit ipsum. Tempor mi leo dolor egestas feugiat risus ultrices? Sit eu tellus suspendisse lacus nulla magna cras fusce. Nulla et venenatis potenti nullam felis laoreet. Nulla magna tellus platea urna laoreet. Vitae venenatis litora, turpis ridiculus cursus facilisi. Dis augue taciti faucibus fermentum a montes cras. Gravida platea vivamus luctus penatibus conubia orci adipiscing aptent enim. Parturient ligula nam etiam quisque dolor non quisque fermentum. 22 | 23 | Odio nascetur suspendisse convallis semper tincidunt mi. Turpis aenean vehicula taciti gravida dapibus suscipit, netus mattis. Aenean diam sagittis elit; nisl ut commodo neque ornare nam. Donec metus cubilia neque per malesuada sit per hendrerit. Ridiculus hac risus lacinia pulvinar mattis? Eleifend torquent fermentum semper nibh duis amet facilisis porta. -------------------------------------------------------------------------------- /tests/if_change_then_change/no_ictc.file: -------------------------------------------------------------------------------- 1 | Lorem ipsum odor amet, consectetuer adipiscing elit. Ornare elit nostra sit metus arcu maximus conubia. Nullam nec elit justo aenean penatibus. Vestibulum cras euismod orci morbi phasellus luctus, sit tincidunt dictum. Hendrerit fringilla augue morbi vel pharetra per fames. Nostra elit mi auctor suspendisse congue. Aptent magnis molestie dui integer turpis quam accumsan? Aenean quis lobortis per tristique mi. 2 | 3 | Platea platea donec duis gravida dapibus. Sodales iaculis eu pharetra finibus magna orci condimentum. Habitant nascetur praesent lobortis etiam vestibulum sollicitudin magna. Augue fames laoreet rutrum vel leo donec hendrerit senectus. Senectus malesuada efficitur suscipit; platea ipsum placerat. Bibendum platea mattis nullam feugiat lorem nibh augue. Nullam aliquet libero non mollis ridiculus blandit. Nisl lacus porta justo eu enim euismod facilisi curabitur tortor. Donec augue nunc aliquam ullamcorper faucibus arcu imperdiet suspendisse. 4 | 5 | Fusce sem sem volutpat phasellus congue quisque ante libero dictumst. Aposuere urna cubilia mi vitae felis velit. Aenean netus habitant molestie blandit pellentesque laoreet. Et est sed; cubilia adipiscing morbi suspendisse ac etiam. Amet facilisi ultrices vulputate arcu, inceptos taciti arcu. Mi malesuada eget sagittis morbi nisl nisi nunc platea. Ac convallis conubia, a urna auctor consectetur eu proin purus. Sollicitudin mollis natoque molestie morbi vulputate nascetur ante posuere phasellus. Neque finibus vestibulum class porttitor nullam tempor massa dis fusce. Netus ornare facilisi nisi molestie tortor. 6 | 7 | Mus facilisis scelerisque quam semper metus orci gravida interdum mattis. Primis semper aliquet blandit condimentum pretium mus. Fames finibus torquent interdum vitae eu. At condimentum a eros turpis mus eros. Diam at interdum euismod conubia dapibus finibus fames adipiscing? Laoreet ut vehicula metus risus eros mollis mauris habitant. Mi elit ante; ullamcorper faucibus dictumst felis ac mattis. Nascetur arcu varius libero convallis penatibus aenean nunc. Molestie velit magnis turpis potenti massa suspendisse. 8 | 9 | Mollis ad litora adipiscing orci mi curae, vitae ipsum auctor. Malesuada aliquam tincidunt senectus justo volutpat elit taciti. Tortor massa malesuada pulvinar; eleifend vivamus sem enim. Nostra iaculis elit fusce dui rutrum sit nulla convallis. Semper pulvinar tellus magnis sociosqu nunc primis aliquam aptent. Nulla lobortis ridiculus dui ante libero sed. Gravida ut eros massa; hendrerit iaculis nisl. Ex dapibus ornare primis duis bibendum; in et cubilia. 10 | 11 | Feugiat conubia sem potenti nec sed elementum torquent. Turpis consequat orci scelerisque; nunc purus arcu hac eu. Dapibus arcu nascetur mi posuere dictum adipiscing. Sed ex lobortis commodo elit facilisis fusce. Varius senectus mattis est tortor; proin ex. Ridiculus curabitur mattis senectus euismod primis ac. Felis dictum suspendisse fringilla sit varius. Nunc risus nullam magna laoreet fusce hendrerit sed sollicitudin. Maecenas eu id mollis felis id suspendisse. Posuere nisl velit egestas porttitor lectus curae. 12 | 13 | Lacus eleifend euismod leo mauris blandit dapibus faucibus. Neque non lectus vel class dapibus per adipiscing pulvinar. Vehicula risus vulputate commodo tincidunt vivamus pretium. Ultricies lectus neque vestibulum primis augue justo vivamus. Ridiculus maximus id mollis facilisis fermentum nulla sapien; vitae montes. Etiam tempus conubia maecenas habitant nec. Nam platea turpis inceptos sapien tristique. Libero maximus porta hac donec scelerisque tempus. Scelerisque velit blandit per tempus ipsum. 14 | 15 | Posuere maximus himenaeos, ut aenean gravida auctor. Nulla ullamcorper fusce sodales; iaculis ac habitasse lacus. Turpis est augue gravida; gravida proin senectus mollis inceptos. Vulputate risus risus tempus class nam aenean erat. Mattis tempor commodo leo felis class natoque egestas imperdiet dis. Vestibulum tristique class posuere nibh iaculis. Sem rhoncus elit pellentesque facilisis auctor. Sodales rhoncus faucibus adipiscing phasellus magnis volutpat placerat magna. 16 | 17 | Eros laoreet porttitor porta etiam metus. Tempus eget integer cras sed non vitae hendrerit scelerisque. Pharetra pulvinar ex semper donec diam dolor. Curabitur vel vitae cubilia curabitur mollis nostra himenaeos. Habitasse volutpat fusce euismod enim bibendum magna. Ac eget bibendum semper lectus class duis gravida metus dictumst. Consequat erat praesent dis netus platea condimentum gravida. Ex platea ipsum ullamcorper duis egestas ullamcorper suscipit mauris vitae. 18 | 19 | Semper finibus enim praesent tempus malesuada at elementum. Sem congue fusce litora, iaculis viverra aenean curabitur semper. In lectus ut libero vehicula curabitur conubia sodales. Pellentesque magna diam ridiculus; netus ad accumsan convallis mattis. Erat euismod feugiat posuere diam hendrerit. Imperdiet himenaeos varius efficitur at nibh interdum. Eros feugiat convallis gravida sodales tincidunt rutrum semper blandit. Nostra varius condimentum per suspendisse, dignissim morbi placerat ridiculus donec. Libero parturient massa mi nascetur volutpat tristique nibh. 20 | 21 | Porttitor magna litora tortor nibh aliquam sit ipsum. Tempor mi leo dolor egestas feugiat risus ultrices? Sit eu tellus suspendisse lacus nulla magna cras fusce. Nulla et venenatis potenti nullam felis laoreet. Nulla magna tellus platea urna laoreet. Vitae venenatis litora, turpis ridiculus cursus facilisi. Dis augue taciti faucibus fermentum a montes cras. Gravida platea vivamus luctus penatibus conubia orci adipiscing aptent enim. Parturient ligula nam etiam quisque dolor non quisque fermentum. 22 | 23 | Odio nascetur suspendisse convallis semper tincidunt mi. Turpis aenean vehicula taciti gravida dapibus suscipit, netus mattis. Aenean diam sagittis elit; nisl ut commodo neque ornare nam. Donec metus cubilia neque per malesuada sit per hendrerit. Ridiculus hac risus lacinia pulvinar mattis? Eleifend torquent fermentum semper nibh duis amet facilisis porta. -------------------------------------------------------------------------------- /tests/if_change_then_change_test.rs: -------------------------------------------------------------------------------- 1 | use spectral::prelude::*; 2 | 3 | mod integration_testing; 4 | use integration_testing::TestRepo; 5 | use std::path::PathBuf; 6 | 7 | use horton::rules::if_change_then_change::find_ictc_blocks; 8 | use horton::rules::if_change_then_change::ThenChange; 9 | 10 | fn assert_no_expected_changes(before: &str, after: &str) -> anyhow::Result<()> { 11 | let test_repo = TestRepo::make().unwrap(); 12 | 13 | test_repo.write("constant.foo", "lorem ipsum".as_bytes()); 14 | test_repo.write("revision.foo", before.as_bytes()); 15 | test_repo.git_commit_all("create constant.foo and revision.foo"); 16 | 17 | test_repo.write("revision.foo", after.as_bytes()); 18 | let horton = test_repo.run_horton()?; 19 | 20 | print!("{}", horton.stdout); 21 | 22 | assert_that(&horton.exit_code).contains_value(0); 23 | assert_that(&horton.has_result( 24 | "if-change-then-change-this", 25 | "Expected change in constant.foo because revision.foo was modified", 26 | Some("revision.foo"), 27 | )) 28 | .is_false(); 29 | 30 | Ok(()) 31 | } 32 | 33 | fn assert_expected_change_in_constant_foo(before: &str, after: &str) -> anyhow::Result<()> { 34 | let test_repo = TestRepo::make().unwrap(); 35 | 36 | test_repo.write("constant.foo", "lorem ipsum".as_bytes()); 37 | test_repo.write("revision.foo", before.as_bytes()); 38 | test_repo.git_commit_all("create constant.foo and revision.foo"); 39 | 40 | test_repo.write("revision.foo", after.as_bytes()); 41 | let horton = test_repo.run_horton()?; 42 | 43 | assert_that(&horton.exit_code).contains_value(0); 44 | assert_that(&horton.has_result( 45 | "if-change-then-change-this", 46 | "Expected change in constant.foo because revision.foo was modified", 47 | Some("revision.foo"), 48 | )) 49 | .is_true(); 50 | Ok(()) 51 | } 52 | 53 | #[test] 54 | fn unmodified_block_and_preceding_lines_unchanged() -> anyhow::Result<()> { 55 | let before = r#" 56 | a 57 | b 58 | c 59 | // IfChange 60 | d 61 | // ThenChange constant.foo 62 | e 63 | f 64 | "#; 65 | 66 | let after = r#" 67 | a 68 | b 69 | c 70 | // IfChange 71 | d 72 | // ThenChange constant.foo 73 | e 74 | f 75 | "#; 76 | 77 | assert_no_expected_changes(&before, &after) 78 | } 79 | 80 | #[test] 81 | fn unmodified_block_and_preceding_lines_changed() -> anyhow::Result<()> { 82 | let before = r#" 83 | a 84 | b 85 | c 86 | // IfChange 87 | d 88 | // ThenChange constant.foo 89 | e 90 | f 91 | "#; 92 | 93 | let after = r#" 94 | a 95 | b 96 | c 97 | x 98 | y 99 | z 100 | // IfChange 101 | d 102 | // ThenChange constant.foo 103 | e 104 | f 105 | "#; 106 | 107 | assert_no_expected_changes(&before, &after) 108 | } 109 | 110 | #[test] 111 | fn unmodified_block_and_preceding_lines_deleted() -> anyhow::Result<()> { 112 | let before = r#" 113 | a 114 | b 115 | c 116 | // IfChange 117 | d 118 | // ThenChange constant.foo 119 | e 120 | f 121 | "#; 122 | 123 | let after = r#" 124 | a 125 | // IfChange 126 | d 127 | // ThenChange constant.foo 128 | e 129 | f 130 | "#; 131 | 132 | assert_no_expected_changes(&before, &after) 133 | } 134 | 135 | #[test] 136 | fn unmodified_block_and_otehr_lines_modified() -> anyhow::Result<()> { 137 | let before = r#" 138 | a 139 | b 140 | c 141 | // IfChange 142 | d 143 | // ThenChange constant.foo 144 | e 145 | f 146 | "#; 147 | 148 | let after = r#" 149 | aaaa 150 | bbb 151 | ccc 152 | // IfChange 153 | d 154 | // ThenChange constant.foo 155 | eeeeeee 156 | ffff 157 | "#; 158 | 159 | assert_no_expected_changes(&before, &after) 160 | } 161 | 162 | #[test] 163 | fn modified_block_and_preceding_lines_unchanged() -> anyhow::Result<()> { 164 | let before = r#" 165 | a 166 | // IfChange 167 | b 168 | // ThenChange constant.foo 169 | c 170 | "#; 171 | 172 | let after = r#" 173 | a 174 | // IfChange 175 | bbbbbbbb 176 | // ThenChange constant.foo 177 | c 178 | "#; 179 | 180 | assert_expected_change_in_constant_foo(&before, &after) 181 | } 182 | 183 | #[test] 184 | fn modified_block_and_preceding_line_count_unchanged() -> anyhow::Result<()> { 185 | let before = r#" 186 | aaaaaaaaaa 187 | // IfChange 188 | b 189 | // ThenChange constant.foo 190 | c 191 | "#; 192 | 193 | let after = r#" 194 | a 195 | // IfChange 196 | bbbbbbbb 197 | // ThenChange constant.foo 198 | c 199 | "#; 200 | 201 | assert_expected_change_in_constant_foo(&before, &after) 202 | } 203 | 204 | #[test] 205 | fn modified_block_and_preceding_line_count_decreased() -> anyhow::Result<()> { 206 | let before = r#" 207 | a 208 | aaa 209 | aaaaa 210 | // IfChange 211 | b 212 | // ThenChange constant.foo 213 | c 214 | "#; 215 | 216 | let after = r#" 217 | a 218 | // IfChange 219 | bbbbbbbb 220 | // ThenChange constant.foo 221 | c 222 | "#; 223 | 224 | assert_expected_change_in_constant_foo(&before, &after) 225 | } 226 | 227 | #[test] 228 | fn modified_block_and_preceding_line_count_increased() -> anyhow::Result<()> { 229 | let before = r#" 230 | a 231 | // IfChange 232 | b 233 | // ThenChange constant.foo 234 | c 235 | "#; 236 | 237 | let after = r#" 238 | a 239 | aa 240 | aaa 241 | // IfChange 242 | bbbbbbbb 243 | // ThenChange constant.foo 244 | c 245 | "#; 246 | 247 | assert_expected_change_in_constant_foo(&before, &after) 248 | } 249 | 250 | #[test] 251 | fn assert_missing_thenchange() { 252 | let single_tag = r#" 253 | aaaa 254 | bbb 255 | cc 256 | // IfChange 257 | d 258 | "#; 259 | 260 | let multi_tag = r#" 261 | aaaa 262 | bbb 263 | cc 264 | // IfChange 265 | c 266 | // IfChange 267 | d 268 | // ThenChange constants.foo 269 | "#; 270 | 271 | let test_repo = TestRepo::make().unwrap(); 272 | 273 | test_repo.write("constant.foo", "foo-bar".as_bytes()); 274 | test_repo.write("revision.foo", "".as_bytes()); 275 | test_repo.git_commit_all("create constant.foo and revision.foo"); 276 | 277 | { 278 | test_repo.write("revision.foo", single_tag.as_bytes()); 279 | let horton = test_repo.run_horton().unwrap(); 280 | assert_that(&horton.exit_code).contains_value(0); 281 | assert_that(&horton.has_result( 282 | "if-change-mismatched", 283 | "Expected matching ThenChange tag", 284 | None, 285 | )) 286 | .is_true(); 287 | } 288 | 289 | { 290 | test_repo.write("revision.foo", multi_tag.as_bytes()); 291 | let horton = test_repo.run_horton().unwrap(); 292 | assert_that(&horton.exit_code).contains_value(0); 293 | assert_that(&horton.has_result( 294 | "if-change-mismatched", 295 | "Expected matching ThenChange tag", 296 | None, 297 | )) 298 | .is_true(); 299 | } 300 | } 301 | 302 | #[test] 303 | fn assert_missing_ifchange() { 304 | let single_tag = r#" 305 | aaaa 306 | bbb 307 | cc 308 | // ThenChange 309 | d 310 | "#; 311 | 312 | let multi_tag = r#" 313 | aaaa 314 | bbb 315 | cc 316 | // IfChange 317 | c 318 | // ThenChange constants.foo 319 | d 320 | // ThenChange constants.foo 321 | "#; 322 | 323 | let test_repo = TestRepo::make().unwrap(); 324 | 325 | test_repo.write("constant.foo", "foo-bar".as_bytes()); 326 | test_repo.write("revision.foo", "".as_bytes()); 327 | test_repo.git_commit_all("create constant.foo and revision.foo"); 328 | 329 | { 330 | test_repo.write("revision.foo", single_tag.as_bytes()); 331 | let horton = test_repo.run_horton().unwrap(); 332 | assert_that(&horton.exit_code).contains_value(0); 333 | assert_that(&horton.has_result( 334 | "if-change-mismatched", 335 | "Expected preceding IfChange tag", 336 | None, 337 | )) 338 | .is_true(); 339 | assert_that(&horton.stdout).contains(""); 340 | } 341 | 342 | { 343 | test_repo.write("revision.foo", multi_tag.as_bytes()); 344 | let horton = test_repo.run_horton().unwrap(); 345 | assert_that(&horton.exit_code).contains_value(0); 346 | assert_that(&horton.has_result( 347 | "if-change-mismatched", 348 | "Expected preceding IfChange tag", 349 | None, 350 | )) 351 | .is_true(); 352 | } 353 | } 354 | 355 | #[test] 356 | fn assert_localfile_notfound() { 357 | let missing_file = r#" 358 | aaaa 359 | bbb 360 | // IfChange 361 | cc 362 | // ThenChange zee.foo 363 | d 364 | "#; 365 | 366 | let test_repo = TestRepo::make().unwrap(); 367 | 368 | test_repo.write("revision.foo", "".as_bytes()); 369 | test_repo.git_commit_all("create constant.foo and revision.foo"); 370 | 371 | { 372 | test_repo.write("revision.foo", missing_file.as_bytes()); 373 | let horton = test_repo.run_horton().unwrap(); 374 | assert_that(&horton.exit_code).contains_value(0); 375 | assert_that(&horton.has_result( 376 | "if-change-file-does-not-exist", 377 | "ThenChange zee.foo does not exist", 378 | None, 379 | )) 380 | .is_true(); 381 | } 382 | } 383 | 384 | #[test] 385 | fn verify_find_ictc_blocks() { 386 | let result = find_ictc_blocks(&PathBuf::from( 387 | "tests/if_change_then_change/basic_ictc.file", 388 | )); 389 | assert!(result.is_ok()); 390 | assert!(result.unwrap().len() == 1, "should find 1 ictc block"); 391 | 392 | let result = find_ictc_blocks(&PathBuf::from("tests/if_change_then_change/no_ictc.file")); 393 | assert!(result.is_ok()); 394 | assert!(result.unwrap().len() == 0, "should find no ictc block"); 395 | 396 | let result = find_ictc_blocks(&PathBuf::from( 397 | "tests/if_change_then_change/multiple_ictc.file", 398 | )); 399 | assert!(result.is_ok()); 400 | let list = result.unwrap(); 401 | assert!(list.len() == 2, "should find two ictc block"); 402 | // assert!(list[0].begin == 1, "first block should point to 2"); 403 | let first = &list[0]; 404 | assert_eq!(first.begin, Some(6)); 405 | assert_eq!(first.end, Some(10)); 406 | match &first.thenchange { 407 | Some(ThenChange::RepoFile(path)) => { 408 | assert_eq!(*path, PathBuf::from("foo.bar")); 409 | } 410 | _ => { 411 | panic!("wrong thenchange type"); 412 | } 413 | }; 414 | 415 | let second = &list[1]; 416 | assert_eq!(second.begin, Some(16)); 417 | assert_eq!(second.end, Some(18)); 418 | match &second.thenchange { 419 | Some(ThenChange::RepoFile(path)) => { 420 | assert_eq!(*path, PathBuf::from("path/to/file/something.else")); 421 | } 422 | _ => { 423 | panic!("wrong thenchange type"); 424 | } 425 | }; 426 | } 427 | -------------------------------------------------------------------------------- /tests/integration_testing.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | 3 | use serde_sarif::sarif::{Run, Sarif}; 4 | use std::fmt; 5 | use std::fs; 6 | use std::process::Command; 7 | use tempfile::NamedTempFile; 8 | 9 | pub struct TestRepo { 10 | dir: tempfile::TempDir, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct HortonOutput { 15 | pub stdout: String, 16 | pub stderr: String, 17 | pub results: String, // results get written to tmp file and then read back in 18 | pub exit_code: Option, 19 | } 20 | 21 | impl HortonOutput { 22 | #[allow(dead_code)] 23 | pub fn runs(&self) -> Vec { 24 | let sarif: Sarif = match serde_json::from_str(&self.results) { 25 | Ok(s) => s, 26 | Err(e) => panic!("Failed to parse stdout as SARIF: {}", e), // Panic if parsing fails 27 | }; 28 | 29 | sarif.runs 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub fn has_result(&self, rule_id: &str, message: &str, file: Option<&str>) -> bool { 34 | // Iterate over the runs and results to find the matching code and message 35 | for run in self.runs() { 36 | if let Some(results) = run.results { 37 | for result in results { 38 | if result.rule_id.as_deref() == Some(rule_id) { 39 | if let Some(text) = result.message.text.as_deref() { 40 | if text.contains(message) { 41 | if file.is_some() { 42 | if let Some(locations) = result.locations { 43 | for location in locations { 44 | if let Some(ph) = location.physical_location { 45 | if let Some(fp) = ph.artifact_location { 46 | if let Some(f) = fp.uri { 47 | if f.contains(file.unwrap()) { 48 | return true; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } else { 56 | return true; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | false 66 | } 67 | 68 | #[allow(dead_code)] 69 | pub fn has_result_with_rule_id(&self, rule_id: &str) -> bool { 70 | // Iterate over the runs and results to find the matching code and message 71 | for run in self.runs() { 72 | if let Some(results) = run.results { 73 | for result in results { 74 | if result.rule_id.as_deref() == Some(rule_id) { 75 | return true; 76 | } 77 | } 78 | } 79 | } 80 | 81 | false 82 | } 83 | } 84 | 85 | impl fmt::Display for HortonOutput { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | match self.exit_code { 88 | Some(c) => write!(f, "toolbox exit code was {}\n", c)?, 89 | None => write!(f, "toolbox exited abnormally\n")?, 90 | }; 91 | 92 | if self.stdout.is_empty() { 93 | write!(f, "toolbox stdout: (empty)\n")?; 94 | } else { 95 | write!(f, "toolbox stdout:\n{}\n", self.stdout.as_str())?; 96 | } 97 | 98 | if self.stderr.is_empty() { 99 | write!(f, "toolbox stderr: (empty)\n") 100 | } else { 101 | write!(f, "toolbox stderr:\n{}\n", self.stderr.as_str()) 102 | } 103 | } 104 | } 105 | 106 | impl TestRepo { 107 | pub fn make() -> anyhow::Result { 108 | // TODO: tempdir is a poor choice: 109 | // 110 | // * created directories do not clearly map to test cases, so 111 | // debugging is hard 112 | // * capturing the tempdir at the end of the test requires 113 | // hooking into the TestRepo dtor to cp -r its contents out 114 | // 115 | // The only thing it does well is clean up after itself. 116 | let dir = tempfile::tempdir()?; 117 | 118 | Command::new("git") 119 | .arg("init") 120 | .arg("--initial-branch") 121 | .arg("main") 122 | .current_dir(dir.path()) 123 | .output()?; 124 | 125 | Command::new("git") 126 | .arg("config") 127 | .arg("user.name") 128 | .arg("horton integration test") 129 | .current_dir(dir.path()) 130 | .output()?; 131 | 132 | Command::new("git") 133 | .arg("config") 134 | .arg("user.email") 135 | .arg("horton@whoville.trunk.io") 136 | .current_dir(dir.path()) 137 | .output()?; 138 | 139 | Command::new("git") 140 | .arg("commit") 141 | .arg("--message") 142 | .arg("Initial commit") 143 | .arg("--allow-empty") 144 | .current_dir(dir.path()) 145 | .output()?; 146 | 147 | Ok(TestRepo { dir }) 148 | } 149 | 150 | pub fn write(&self, relpath: &str, data: &[u8]) { 151 | let path = { 152 | let mut path = self.dir.path().to_path_buf(); 153 | path.push(relpath); 154 | path 155 | }; 156 | 157 | // Create the directory hierarchy if needed 158 | if let Some(parent) = path.parent() { 159 | fs::create_dir_all(parent) 160 | .expect(format!("Unable to create directories for {:#?}", parent).as_str()); 161 | } 162 | 163 | fs::write(&path, data).expect(format!("Unable to write {:#?}", path).as_str()); 164 | } 165 | 166 | #[allow(dead_code)] 167 | pub fn delete(&self, relpath: &str) { 168 | let path = { 169 | let mut path = self.dir.path().to_path_buf(); 170 | path.push(relpath); 171 | path 172 | }; 173 | fs::remove_file(&path).expect(format!("Unable to delete {:#?}", path).as_str()); 174 | } 175 | 176 | pub fn git_add_all(&self) -> anyhow::Result<()> { 177 | Command::new("git") 178 | .arg("add") 179 | .arg(".") 180 | .current_dir(self.dir.path()) 181 | .output()?; 182 | 183 | Ok(()) 184 | } 185 | 186 | #[allow(dead_code)] 187 | pub fn git_commit_all(&self, message: &str) { 188 | self.git_add_all().expect("add worked"); 189 | 190 | let output = Command::new("git") 191 | .arg("commit") 192 | .arg("-m") 193 | .arg(message) 194 | .current_dir(self.dir.path()) 195 | .output() 196 | .expect("Failed to execute git command"); 197 | 198 | assert!(output.status.success(), "Git commit failed"); 199 | } 200 | 201 | #[allow(dead_code)] 202 | pub fn run_horton(&self) -> anyhow::Result { 203 | self.run_horton_with("HEAD", "sarif", true) 204 | } 205 | 206 | #[allow(dead_code)] 207 | pub fn set_toolbox_toml(&self, config: &str) { 208 | self.write(".config/toolbox.toml", config.as_bytes()); 209 | } 210 | 211 | pub fn run_horton_with( 212 | &self, 213 | upstream_ref: &str, 214 | format: &str, 215 | write_results_to_file: bool, 216 | ) -> anyhow::Result { 217 | let mut cmd = Command::cargo_bin("trunk-toolbox")?; 218 | 219 | let modified_paths = 220 | horton::git::modified_since(upstream_ref, Some(self.dir.path()))?.paths; 221 | let files: Vec = modified_paths.keys().map(|key| key.to_string()).collect(); 222 | 223 | cmd.arg("--upstream") 224 | .arg(upstream_ref) 225 | .current_dir(self.dir.path()); 226 | cmd.arg("--output-format").arg(format); 227 | for path in files { 228 | cmd.arg(path); 229 | } 230 | 231 | let tmpfile_path = if write_results_to_file { 232 | // create a temporary file 233 | let tmpfile = NamedTempFile::new()?; 234 | let path = tmpfile.path().to_str().unwrap().to_string(); 235 | cmd.arg("--results").arg(tmpfile.path().to_str().unwrap()); 236 | path 237 | } else { 238 | String::new() 239 | }; 240 | 241 | log::debug!("Command: {}", format!("{:?}", cmd)); 242 | 243 | let output = cmd.output()?; 244 | 245 | let results = if write_results_to_file { 246 | fs::read_to_string(tmpfile_path)? 247 | } else { 248 | String::new() 249 | }; 250 | 251 | return Ok(HortonOutput { 252 | stdout: String::from_utf8(output.stdout)?, 253 | stderr: String::from_utf8(output.stderr)?, 254 | results, 255 | exit_code: output.status.code(), 256 | }); 257 | } 258 | } 259 | 260 | impl Drop for TestRepo { 261 | fn drop(&mut self) { 262 | log::info!( 263 | "TestRepo will clean up after itself: {:#?}", 264 | self.dir.path() 265 | ); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /tests/main_test.rs: -------------------------------------------------------------------------------- 1 | use spectral::prelude::*; 2 | 3 | mod integration_testing; 4 | use integration_testing::TestRepo; 5 | 6 | #[test] 7 | fn binary_file_untracked() -> anyhow::Result<()> { 8 | let test_repo = TestRepo::make()?; 9 | 10 | test_repo.write("picture.binary", include_bytes!("trunk-logo.png")); 11 | 12 | let horton = test_repo.run_horton()?; 13 | 14 | assert_that(&horton.exit_code).contains_value(0); 15 | assert_that(&horton.stdout.contains("Expected change")).is_false(); 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn binary_file_committed() -> anyhow::Result<()> { 21 | let test_repo = TestRepo::make()?; 22 | 23 | test_repo.write("picture.binary", include_bytes!("trunk-logo.png")); 24 | test_repo.git_commit_all("commit a picture"); 25 | 26 | let horton = test_repo.run_horton_with("HEAD^", "sarif", false)?; 27 | 28 | print!("{}", horton.stdout); 29 | 30 | assert_that(&horton.exit_code).contains_value(0); 31 | assert_that(&horton.stdout.contains("Expected change")).is_false(); 32 | 33 | Ok(()) 34 | } 35 | 36 | #[test] 37 | fn lfs_file_untracked() -> anyhow::Result<()> { 38 | let test_repo = TestRepo::make()?; 39 | 40 | test_repo.write( 41 | ".gitattributes", 42 | "*.binary filter=lfs diff=lfs merge=lfs -text\n".as_bytes(), 43 | ); 44 | test_repo.git_commit_all("create .gitattributes"); 45 | 46 | test_repo.write("picture.binary", include_bytes!("trunk-logo.png")); 47 | 48 | let horton = test_repo.run_horton()?; 49 | 50 | assert_that(&horton.exit_code).contains_value(0); 51 | assert_that(&horton.stdout.contains("Expected change")).is_false(); 52 | 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn lfs_file_committed() -> anyhow::Result<()> { 58 | let test_repo = TestRepo::make()?; 59 | 60 | test_repo.write( 61 | ".gitattributes", 62 | "*.binary filter=lfs diff=lfs merge=lfs -text\n".as_bytes(), 63 | ); 64 | test_repo.git_commit_all("create .gitattributes"); 65 | 66 | test_repo.write("picture.binary", include_bytes!("trunk-logo.png")); 67 | 68 | let horton = test_repo.run_horton_with("HEAD^", "sarif", false)?; 69 | 70 | assert_that(&horton.exit_code).contains_value(0); 71 | assert_that(&horton.stdout.contains("Expected change")).is_false(); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /tests/never_edit_test.rs: -------------------------------------------------------------------------------- 1 | use spectral::prelude::*; 2 | 3 | mod integration_testing; 4 | use integration_testing::TestRepo; 5 | 6 | #[test] 7 | fn assert_modified_locked_file() -> anyhow::Result<()> { 8 | let test_repo: TestRepo = TestRepo::make().unwrap(); 9 | 10 | test_repo.write("src/write_once.txt", "immutable text".as_bytes()); 11 | test_repo.write("src/write_many.txt", "immutable text".as_bytes()); 12 | test_repo.git_add_all()?; 13 | test_repo.git_commit_all("create write once and write many file"); 14 | 15 | // enable and configure never_edit 16 | let toml = r#" 17 | [neveredit] 18 | enabled = true 19 | paths = ["src/foo", "src/bar/**", "**/write_once*"] 20 | "#; 21 | 22 | // write to the protected file 23 | test_repo.write("src/write_once.txt", "edit the text".as_bytes()); 24 | test_repo.write("src/write_many.txt", "edit the text".as_bytes()); 25 | 26 | test_repo.set_toolbox_toml(toml); 27 | 28 | let horton = test_repo.run_horton()?; 29 | 30 | assert_that(&horton.exit_code).contains_value(0); 31 | assert_that(&horton.has_result( 32 | "never-edit-modified", 33 | "file is protected and should not be modified", 34 | Some("src/write_once.txt"), 35 | )) 36 | .is_true(); 37 | assert_that(&horton.has_result("never-edit-modified", "", Some("src/write_many.txt"))) 38 | .is_false(); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[test] 44 | fn assert_deleted_locked_file() -> anyhow::Result<()> { 45 | let test_repo: TestRepo = TestRepo::make().unwrap(); 46 | 47 | test_repo.write("src/locked/file.txt", "immutable text".as_bytes()); 48 | test_repo.write("src/editable.txt", "mutable text".as_bytes()); 49 | test_repo.git_add_all()?; 50 | test_repo.git_commit_all("create locked and editable files"); 51 | 52 | // enable and configure never_edit 53 | let toml = r#" 54 | [neveredit] 55 | enabled = true 56 | paths = ["src/locked/**"] 57 | "#; 58 | 59 | // write to the protected file 60 | test_repo.delete("src/locked/file.txt"); 61 | 62 | test_repo.set_toolbox_toml(toml); 63 | 64 | let horton = test_repo.run_horton()?; 65 | 66 | assert_that(&horton.exit_code).contains_value(0); 67 | assert_that(&horton.has_result("never-edit-deleted", "", Some("src/locked/file.txt"))) 68 | .is_true(); 69 | 70 | Ok(()) 71 | } 72 | 73 | #[test] 74 | fn honor_disabled_in_config() -> anyhow::Result<()> { 75 | let test_repo: TestRepo = TestRepo::make().unwrap(); 76 | 77 | test_repo.write("src/locked/file.txt", "immutable text".as_bytes()); 78 | test_repo.git_add_all()?; 79 | test_repo.git_commit_all("create locked and editable files"); 80 | 81 | let toml_on = r#" 82 | [neveredit] 83 | enabled = true 84 | paths = ["src/locked/**"] 85 | "#; 86 | 87 | // enable and configure never_edit 88 | let toml_off = r#" 89 | [neveredit] 90 | enabled = false 91 | paths = ["src/locked/**"] 92 | "#; 93 | 94 | // write to the protected file 95 | test_repo.delete("src/locked/file.txt"); 96 | 97 | test_repo.set_toolbox_toml(toml_on); 98 | let mut horton = test_repo.run_horton()?; 99 | assert_that(&horton.has_result("never-edit-deleted", "", Some("src/locked/file.txt"))) 100 | .is_true(); 101 | 102 | test_repo.set_toolbox_toml(toml_off); 103 | horton = test_repo.run_horton()?; 104 | 105 | assert_that(&horton.exit_code).contains_value(0); 106 | assert_that(&horton.has_result("never-edit-deleted", "", Some("src/locked/file.txt"))) 107 | .is_false(); 108 | assert_that(&horton.has_result("toolbox-perf", "1 files processed", None)).is_true(); 109 | 110 | Ok(()) 111 | } 112 | 113 | #[test] 114 | fn warn_for_config_not_protecting_anything() -> anyhow::Result<()> { 115 | let test_repo: TestRepo = TestRepo::make().unwrap(); 116 | 117 | // enable and configure never_edit 118 | let toml = r#" 119 | [neveredit] 120 | enabled = true 121 | paths = ["bad_path/**"] 122 | "#; 123 | test_repo.set_toolbox_toml(toml); 124 | 125 | let horton: integration_testing::HortonOutput = test_repo.run_horton()?; 126 | 127 | assert_that(&horton.exit_code).contains_value(0); 128 | assert_that(&horton.has_result_with_rule_id("never-edit-bad-config")).is_true(); 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /tests/no_curly_quote_test.rs: -------------------------------------------------------------------------------- 1 | use spectral::prelude::*; 2 | 3 | mod integration_testing; 4 | use integration_testing::TestRepo; 5 | 6 | const TOML_ON: &str = r#" 7 | [nocurlyquotes] 8 | enabled = true 9 | "#; 10 | 11 | const CURLY_QUOTES: &str = r#" 12 | the opening double quote ( “ ) U+201C 13 | the closing double quote ( ” ) U+201D 14 | the opening single quote ( ‘ ) U+2018 15 | the closing single quote ( ’) U+2019 16 | the double low quotation ( „ ) U+201E 17 | the double high reversed ( ‟ ) U+201F 18 | // 19 | "#; 20 | 21 | #[test] 22 | fn honor_disabled_in_config() -> anyhow::Result<()> { 23 | let test_repo: TestRepo = TestRepo::make().unwrap(); 24 | 25 | test_repo.write("src/curly.txt", "empty_file".as_bytes()); 26 | test_repo.git_add_all()?; 27 | test_repo.git_commit_all("create curly quote file"); 28 | test_repo.write("src/curly.txt", CURLY_QUOTES.as_bytes()); 29 | 30 | // disable nocurlyquotes 31 | let toml_off = r#" 32 | [nocurlyquotes] 33 | enabled = false 34 | "#; 35 | 36 | test_repo.set_toolbox_toml(TOML_ON); 37 | let mut horton = test_repo.run_horton()?; 38 | assert_that(&horton.has_result("no-curly-quotes", "", Some("src/curly.txt"))).is_true(); 39 | 40 | test_repo.set_toolbox_toml(toml_off); 41 | horton = test_repo.run_horton()?; 42 | 43 | assert_that(&horton.exit_code).contains_value(0); 44 | assert_that(&horton.has_result("no-curly-quotes", "", Some("src/curly.txt"))).is_false(); 45 | assert_that(&horton.has_result("toolbox-perf", "1 files processed", None)).is_true(); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn assert_find_curly_quotes() { 52 | let test_repo = TestRepo::make().unwrap(); 53 | test_repo.set_toolbox_toml(TOML_ON); 54 | test_repo.write("revision.foo", "//".as_bytes()); 55 | test_repo.git_commit_all("create revision.foo"); 56 | 57 | { 58 | test_repo.write("revision.foo", CURLY_QUOTES.as_bytes()); 59 | let horton = test_repo.run_horton().unwrap(); 60 | assert_that(&horton.exit_code).contains_value(0); 61 | assert_that(&horton.has_result( 62 | "no-curly-quotes", 63 | "Found curly quote on line 2", 64 | Some("revision.foo"), 65 | )) 66 | .is_true(); 67 | assert_that(&horton.has_result( 68 | "no-curly-quotes", 69 | "Found curly quote on line 3", 70 | Some("revision.foo"), 71 | )) 72 | .is_true(); 73 | assert_that(&horton.has_result( 74 | "no-curly-quotes", 75 | "Found curly quote on line 4", 76 | Some("revision.foo"), 77 | )) 78 | .is_true(); 79 | assert_that(&horton.has_result( 80 | "no-curly-quotes", 81 | "Found curly quote on line 5", 82 | Some("revision.foo"), 83 | )) 84 | .is_true(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/output_format_test.rs: -------------------------------------------------------------------------------- 1 | // trunk-ignore-all(trunk-toolbox/do-not-land) 2 | extern crate regex; 3 | 4 | use serde_json::Error; 5 | use serde_sarif::sarif::Sarif; 6 | use spectral::prelude::*; 7 | 8 | mod integration_testing; 9 | use integration_testing::TestRepo; 10 | 11 | #[test] 12 | fn default_sarif() -> anyhow::Result<()> { 13 | let test_repo = TestRepo::make()?; 14 | 15 | test_repo.write( 16 | "alpha.foo", 17 | "lorem ipsum dolor\ndo-NOT-lAnD\nDONOTLAND sit amet\n".as_bytes(), 18 | ); 19 | test_repo.git_add_all()?; 20 | let horton = test_repo.run_horton_with("HEAD", "sarif", false)?; 21 | 22 | let sarif: Result = serde_json::from_str(&horton.stdout); 23 | assert_that(&sarif.is_ok()).is_true(); 24 | assert_that(&sarif.unwrap().runs).has_length(1); 25 | 26 | Ok(()) 27 | } 28 | 29 | #[test] 30 | fn default_print() -> anyhow::Result<()> { 31 | let test_repo = TestRepo::make()?; 32 | 33 | test_repo.write( 34 | "alpha.foo", 35 | "lorem ipsum dolor\ndo-NOT-lAnD\nDONOTLAND sit amet\n".as_bytes(), 36 | ); 37 | test_repo.git_add_all()?; 38 | let horton = test_repo.run_horton_with("HEAD", "text", false)?; 39 | let expected_text = String::from( 40 | "alpha.foo:1:0: Found 'do-NOT-lAnD' (error)\nalpha.foo:2:0: Found 'DONOTLAND' (error)\n", 41 | ); 42 | 43 | assert_that(&horton.exit_code).contains_value(0); 44 | assert_that(&horton.stdout).is_equal_to(&expected_text); 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /tests/todo_test.rs: -------------------------------------------------------------------------------- 1 | // trunk-ignore-all(trunk-toolbox/todo) 2 | use spectral::prelude::*; 3 | 4 | mod integration_testing; 5 | use integration_testing::TestRepo; 6 | 7 | fn write_enable_config(test_repo: &TestRepo) -> anyhow::Result<()> { 8 | let config = r#" 9 | [todo] 10 | enabled = true 11 | 12 | [donotland] 13 | enabled = false 14 | "#; 15 | test_repo.write("toolbox.toml", config.as_bytes()); 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn basic_todo() -> anyhow::Result<()> { 21 | let test_repo = TestRepo::make()?; 22 | 23 | test_repo.write( 24 | "alpha.foo", 25 | "lorem ipsum dolor\ntoDO\nsit amet\n".as_bytes(), 26 | ); 27 | write_enable_config(&test_repo)?; 28 | test_repo.git_add_all()?; 29 | let horton = test_repo.run_horton()?; 30 | 31 | assert_that(&horton.exit_code).contains_value(0); 32 | assert_that(&horton.has_result_with_rule_id("todo")).is_true(); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[test] 38 | fn basic_fixme() -> anyhow::Result<()> { 39 | let test_repo = TestRepo::make()?; 40 | 41 | test_repo.write( 42 | "alpha.foo", 43 | "lorem ipsum dolor\nFIXME: fix this\nsit amet\n".as_bytes(), 44 | ); 45 | write_enable_config(&test_repo)?; 46 | test_repo.git_add_all()?; 47 | let horton = test_repo.run_horton()?; 48 | 49 | assert_that(&horton.exit_code).contains_value(0); 50 | assert_that(&horton.has_result("todo", "Found 'FIXME'", Some("alpha.foo"))).is_true(); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn basic_mastodon() -> anyhow::Result<()> { 57 | let test_repo = TestRepo::make()?; 58 | 59 | test_repo.write( 60 | "alpha.foo", 61 | "lorem ipsum dolor\n// Mastodons are cool\nsit amet\n".as_bytes(), 62 | ); 63 | write_enable_config(&test_repo)?; 64 | test_repo.git_add_all()?; 65 | let horton = test_repo.run_horton()?; 66 | 67 | assert_that(&horton.exit_code).contains_value(0); 68 | assert_that(&horton.stdout.contains("Found 'todo'")).is_false(); 69 | 70 | Ok(()) 71 | } 72 | 73 | #[test] 74 | fn default_disabled_in_config() -> anyhow::Result<()> { 75 | let test_repo = TestRepo::make()?; 76 | test_repo.write("alpha.foo", "todo\n".as_bytes()); 77 | test_repo.git_add_all()?; 78 | 79 | { 80 | let horton = test_repo.run_horton()?; 81 | assert_that(&horton.stdout.contains("Found 'todo'")).is_false(); 82 | } 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /tests/trunk-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunk-io/toolbox/41d60280011e0abe2ba55510b6aef66fef13a78d/tests/trunk-logo.png -------------------------------------------------------------------------------- /toolbox-latest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the paths. Don't override in case it's being passed in by trunk check 4 | release_path="${release_path:-./target/release/trunk-toolbox}" 5 | debug_path="${debug_path:-./target/debug/trunk-toolbox}" 6 | fallback_path="trunk-toolbox" 7 | 8 | # Check if the release and debug files exist 9 | if [[ -e ${release_path} && -e ${debug_path} ]]; then 10 | # If both files exist, check which one is more recent 11 | if [[ ${release_path} -nt ${debug_path} ]]; then 12 | # If the release file is more recent, execute it 13 | echo "Executing ${release_path}" 14 | ${release_path} "$@" 15 | exit $? 16 | else 17 | # If the debug file is more recent, execute it 18 | echo "Executing ${debug_path}" 19 | ${debug_path} "$@" 20 | exit $? 21 | fi 22 | elif [[ -e ${release_path} ]]; then 23 | # If only the release file exists, execute it 24 | echo "Executing ${release_path}" 25 | ${release_path} "$@" 26 | exit $? 27 | elif [[ -e ${debug_path} ]]; then 28 | # If only the debug file exists, execute it 29 | echo "Executing ${debug_path}" 30 | ${debug_path} "$@" 31 | exit $? 32 | else 33 | # If neither file exists, execute the fallback path 34 | echo "Executing ${fallback_path}" 35 | ${fallback_path} "$@" 36 | exit $? 37 | fi 38 | --------------------------------------------------------------------------------