├── .github └── workflows │ ├── main.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── book ├── book.toml └── src │ ├── 1_foreword.md │ ├── 2_0_process.md │ ├── 2_1_overview.md │ ├── 2_2_config.md │ ├── 2_3_repository.md │ ├── 2_4_manifest.md │ ├── 2_5_api_extraction.md │ ├── 2_6_comparator.md │ ├── 2_7_diagnosis.md │ ├── 2_8_next_version.md │ ├── 3_book.md │ └── SUMMARY.md ├── docs ├── .nojekyll ├── 1_foreword.html ├── 2_0_process.html ├── 2_1_overview.html ├── 2_2_config.html ├── 2_3_repository.html ├── 2_4_manifest.html ├── 2_5_api_extraction.html ├── 2_6_comparator.html ├── 2_7_diagnosis.html ├── 2_8_next_version.html ├── 3_book.html ├── 404.html ├── CNAME ├── FontAwesome │ ├── css │ │ └── font-awesome.css │ └── fonts │ │ ├── FontAwesome.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── ayu-highlight.css ├── book.js ├── clipboard.min.js ├── css │ ├── chrome.css │ ├── general.css │ ├── print.css │ └── variables.css ├── elasticlunr.min.js ├── favicon.png ├── favicon.svg ├── fonts │ ├── OPEN-SANS-LICENSE.txt │ ├── SOURCE-CODE-PRO-LICENSE.txt │ ├── fonts.css │ ├── open-sans-v17-all-charsets-300.woff2 │ ├── open-sans-v17-all-charsets-300italic.woff2 │ ├── open-sans-v17-all-charsets-600.woff2 │ ├── open-sans-v17-all-charsets-600italic.woff2 │ ├── open-sans-v17-all-charsets-700.woff2 │ ├── open-sans-v17-all-charsets-700italic.woff2 │ ├── open-sans-v17-all-charsets-800.woff2 │ ├── open-sans-v17-all-charsets-800italic.woff2 │ ├── open-sans-v17-all-charsets-italic.woff2 │ ├── open-sans-v17-all-charsets-regular.woff2 │ └── source-code-pro-v11-all-charsets-500.woff2 ├── highlight.css ├── highlight.js ├── index.html ├── mark.min.js ├── print.html ├── searcher.js ├── searchindex.js ├── searchindex.json └── tomorrow-night.css ├── logo-full.svg ├── src ├── ast.rs ├── cli.rs ├── comparator.rs ├── diagnosis.rs ├── git.rs ├── glue.rs ├── lib.rs ├── main.rs ├── manifest.rs ├── public_api.rs └── public_api │ ├── functions.rs │ ├── imports.rs │ ├── methods.rs │ ├── trait_defs.rs │ ├── trait_impls.rs │ ├── types.rs │ └── utils.rs └── tests ├── enum.rs ├── function.rs ├── method.rs ├── structs.rs ├── trait_defs.rs └── trait_impls.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | schedule: 7 | - cron: 0 0 1 * * 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test-linux: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout source 17 | uses: actions/checkout@v2 18 | 19 | - uses: Swatinem/rust-cache@v1 20 | 21 | - name: cargo test 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: test 25 | args: --workspace 26 | 27 | test-osx: 28 | runs-on: macos-latest 29 | steps: 30 | - name: Checkout source 31 | uses: actions/checkout@v2 32 | 33 | - uses: Swatinem/rust-cache@v1 34 | 35 | - name: cargo test 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: test 39 | args: --workspace 40 | 41 | test-windows: 42 | runs-on: windows-latest 43 | steps: 44 | - name: Checkout source 45 | uses: actions/checkout@v2 46 | 47 | - uses: Swatinem/rust-cache@v1 48 | 49 | - name: cargo test 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: test 53 | args: --workspace 54 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | test-linux: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout source 15 | uses: actions/checkout@v2 16 | 17 | - uses: Swatinem/rust-cache@v1 18 | 19 | - name: cargo test 20 | uses: actions-rs/cargo@v1 21 | with: 22 | command: test 23 | args: --workspace 24 | 25 | - name: rustfmt 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: fmt 29 | args: --all -- --check 30 | 31 | - name: clippy 32 | uses: actions-rs/clippy-check@v1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | args: --all-targets --all-features -- -D warnings 36 | 37 | test-osx: 38 | runs-on: macos-latest 39 | steps: 40 | - name: Checkout source 41 | uses: actions/checkout@v2 42 | 43 | - uses: Swatinem/rust-cache@v1 44 | 45 | - name: cargo test 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: test 49 | args: --workspace 50 | 51 | test-windows: 52 | runs-on: windows-latest 53 | steps: 54 | - name: Checkout source 55 | uses: actions/checkout@v2 56 | 57 | - uses: Swatinem/rust-cache@v1 58 | 59 | - name: cargo test 60 | uses: actions-rs/cargo@v1 61 | with: 62 | command: test 63 | args: --workspace 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source 13 | uses: actions/checkout@v2 14 | - name: Install toolchain 15 | uses: hecrj/setup-rust-action@v1 16 | with: 17 | targets: x86_64-unknown-linux-gnu 18 | - name: Build release (Linux) 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: build 22 | args: --release --target=x86_64-unknown-linux-gnu 23 | - run: strip target/x86_64-unknown-linux-gnu/release/cargo-breaking 24 | - uses: actions/upload-artifact@v2 25 | with: 26 | name: build 27 | path: | 28 | target/86_64-unknown-linux-gnu/release/cargo-breaking 29 | 30 | build-osx: 31 | runs-on: macos-latest 32 | steps: 33 | - name: Checkout source 34 | uses: actions/checkout@v2 35 | - name: Build release (OSX) 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: build 39 | args: --release --target=x86_64-apple-darwin 40 | - uses: actions/upload-artifact@v2 41 | with: 42 | name: build-osx 43 | path: | 44 | target/x86_64-apple-darwin/release/cargo-breaking 45 | 46 | build-windows: 47 | runs-on: windows-latest 48 | steps: 49 | - name: Checkout source 50 | uses: action/checkout@v2 51 | - name: Build release (Windows) 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: build 55 | args: --release --target=x86_64-pc-windows-msvc 56 | - uses: actions/upload-artifact@v2 57 | with: 58 | name: build-windows 59 | path: | 60 | target/x86_64-pc-windows-msvc/release/cargo-breaking.exe 61 | 62 | release: 63 | needs: [build-linux, build-osx] 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Get the version 67 | id: get_version 68 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} 69 | - uses: actions/download-artifact@v2 70 | with: 71 | name: build-linux 72 | path: build-linux 73 | - run: mv build-linux/cargo-breaking build-linux/cargo-breaking-${{ steps.get_version.outputs.VERSION }}-linux-x86_64 74 | - uses: actions/download-artifact@v2 75 | with: 76 | name: build-osx 77 | path: build-osx 78 | - run: mv build-osx/cargo-breaking build-osx/cargo-breaking-${{ steps.get_version.outputs.VERSION }}-osx-x86_64 79 | - uses: actions/download-artifact@v2 80 | with: 81 | name: build-windows 82 | path: build-windows 83 | - run: mv build-windows/cargo-breaking.exe build-windows/cargo-breaking-${{ steps.get_version.outputs.VERSION }}-windows-x86_64.exe 84 | - name: Release 85 | uses: softprops/actions-gh-release@v1 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | with: 89 | files: | 90 | build-linux/* 91 | build-osx/* 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, 10 | level of experience, education, socio-economic status, nationality, personal 11 | appearance, race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all project spaces, and it also applies when 50 | an individual is representing the project or its community in public spaces. 51 | Examples of representing a project or community include using an official 52 | project e-mail address, posting via an official social media account, or acting 53 | as an appointed representative at an online or offline event. Representation of 54 | a project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at jeremy.lempereur@gmail.com. All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an 63 | incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 73 | version 1.4, 74 | available at 75 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see 80 | https://www.contributor-covenant.org/faq 81 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-breaking" 3 | version = "0.0.3-alpha.0" 4 | authors = ["o0Ignition0o "] 5 | edition = "2018" 6 | description = "checks the diff between your last publish and the current code, and lets you know if there are breaking changes so you can bump to the right version." 7 | keywords = ["release", "api", "breaking", "change"] 8 | license = "MPL-2.0" 9 | repository = "https://github.com/iomentum/cargo-breaking" 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | 14 | [[bin]] 15 | path = "src/main.rs" 16 | name = "cargo-breaking" 17 | 18 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 19 | 20 | [dependencies] 21 | syn = { version = "1.0", features = ["full", "extra-traits", "visit"] } 22 | anyhow = "1.0" 23 | git2 = "0.13" 24 | cargo_toml = "0.9" 25 | semver = "1.0" 26 | clap = "2.33" 27 | tap = "1.0" 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cargo-breaking` 2 | 3 | ! Disclaimer !: This repository is no longer maintained and our advice is to use https://crates.io/crates/cargo-semver-checks instead. 4 | 5 | 6 |
7 | 8 |
9 | 10 | Logo is provided by Morgane Gaillard (@Arlune) under the MPL license. 11 |
12 | 13 | 14 | `cargo-breaking` compares a crate's public API between two different branches, 15 | shows what changed, and suggests the next version according to [semver][semver]. 16 | 17 | ## Example 18 | 19 | Suppose you're building a crate that, for any reason, deals with users. The 20 | crate version is 2.4.3. You remove the `User::from_str` method, add a new public 21 | field to `User`, implement `Debug` for it and add the `User::from_path` 22 | function. 23 | 24 | When invoked, the following text should be printed: 25 | 26 | ```none 27 | $ cargo breaking 28 | - user::User::from_str 29 | ≠ user::User 30 | + user::User::from_path 31 | + user::User: Debug 32 | 33 | Next version is: 3.0.0 34 | ``` 35 | 36 | ### Args 37 | 38 | `against`, an arg to specify the github ref (a tag, a branch name or a commit) against which we can compare our current crate version. 39 | 40 | - use: 41 | 42 | ```none 43 | cargo breaking -a branch_name 44 | ``` 45 | 46 | - default: "main" 47 | 48 | ## Goals and non goals 49 | 50 | `cargo-breaking` aims to detect most breaking changes, but deliberately chooses 51 | to ignore the most subtle ones. This includes, but is not limited to: 52 | 53 | - when the size of a type changes ([playground example][add-field-pg]), 54 | - when a public trait is implemented for a type (see 55 | [`assert_not_impl_any`][ania]). 56 | 57 | ## Status 58 | 59 | By default, `cargo-breaking` compares the public API of the crate against what 60 | is exposed in the `main` branch. This can be changed with the `--against` 61 | (abbreviated by `-a`) parameter. The value can be a branch name, a tag name, or 62 | a commit SHA-1. 63 | 64 | It currently detects the following: 65 | 66 | - functions, 67 | - struct fields and generic parameters, 68 | - enum variants, fields and generic parameters, 69 | - methods when the implemented type is simple enough. 70 | 71 | As we compare parts of the crate AST, it reports a lot of false positives: 72 | 73 | - renaming an argument is reported as a breaking change, 74 | - renaming a generic type is reported as a breaking change, 75 | - adding a generic type with a default value is a breaking change, 76 | - depending on the situation, adding a trailing comma may be a breaking change. 77 | 78 | [semver]: https://semver.org/ 79 | [add-field-pg]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=492a1727404d1f8d199962c639454f44 80 | [ania]: https://docs.rs/static_assertions/1.1.0/static_assertions/macro.assert_not_impl_any.html 81 | 82 | ## Contribution 83 | 84 | A book is maintained to help understanding how the crate works, and what are its inner parts and their behaviour. 85 | 86 | It can be found here : 87 | [book](https://iomentum.github.io/cargo-breaking/) 88 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["iomentum"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Guide to cargo-breaking Development" 7 | -------------------------------------------------------------------------------- /book/src/1_foreword.md: -------------------------------------------------------------------------------- 1 | # Foreword 2 | 3 | This book's goal is to hold and maintain informations on how the innards of cargo-breaking works together to compare two versions of a library and display the differences between both. 4 | 5 | Example: 6 | 7 | ``` 8 | $ cargo breaking 9 | - user::User::from_str 10 | ≠ user::User 11 | + user::User::from_path 12 | + user::User: Debug 13 | 14 | Next version is: 3.0.0 15 | ``` 16 | 17 | ## Installation 18 | 19 | `cargo-breaking` needs the nightly toolchain to be installed to work correctly, 20 | but can be compiled with any toolchain. It can be compiled from sources with the 21 | following commands: 22 | 23 | ```none 24 | $ git clone https://github.com/iomentum/cargo-breaking 25 | $ cd cargo-breaking 26 | $ cargo install --path ./ 27 | ``` 28 | 29 | You may need to add the `--force` argument to the last command if you're 30 | upgrading from a previous version. 31 | 32 | ### Git workflow 33 | 34 | Most work is commited in separate branch, before getting merged to `main` all 35 | at once, once we're satisfied with the refactoring, fixes, and features added. 36 | These branches are named `scrabsha/iter-dd-mm-yy`, representing the date at 37 | which the iteration is started (for instance, `scrabsha/iter-19-06-21`). 38 | 39 | Installing `cargo-breaking` from the following branches give you the latest 40 | changes. It may have instabilities, though. 41 | -------------------------------------------------------------------------------- /book/src/2_0_process.md: -------------------------------------------------------------------------------- 1 | # Process 2 | 3 | This section describes the flow of the cargo-breaking application. 4 | -------------------------------------------------------------------------------- /book/src/2_1_overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The process used by cargo-breaking can be summarized like this: 4 | 5 | - [2.2](./2_2_config.md): The configuration is parsed from the cli args 6 | 7 | - [2.3](./2_3_repository.md): The git repository informations are created from the env 8 | 9 | - [2.4](./2_4_manifest.md): The crate version is fetched from the manifest 10 | 11 | - [2.5](./2_5_api_extraction.md): The "current library" and the "target library to run against" are collected as AST with rustc 12 | 13 | - [2.6](./2_6_comparator.md): Both libraries are compared against each other to collect removals, additions and modifications 14 | 15 | - [2.7](./2_7_diagnosis.md): The results are gathered in a diagnosis structure 16 | 17 | - [2.8](./2_8_next_version.md): The "best" next version is suggested from the diagnosis 18 | -------------------------------------------------------------------------------- /book/src/2_2_config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_3_repository.md: -------------------------------------------------------------------------------- 1 | # Repository 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_4_manifest.md: -------------------------------------------------------------------------------- 1 | # Manifest 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_5_api_extraction.md: -------------------------------------------------------------------------------- 1 | # API Extraction 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_6_comparator.md: -------------------------------------------------------------------------------- 1 | # Comparator 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_7_diagnosis.md: -------------------------------------------------------------------------------- 1 | # Diagnosis 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/2_8_next_version.md: -------------------------------------------------------------------------------- 1 | # Next Version 2 | 3 | WORK IN PROGRESS -------------------------------------------------------------------------------- /book/src/3_book.md: -------------------------------------------------------------------------------- 1 | # Book 2 | 3 | Mdbook is needed to get running this book: 4 | 5 | ```none 6 | $ cargo install mdbook 7 | $ cd book 8 | $ mdbook serve --dest-dir ../docs 9 | ``` 10 | 11 | ### Building the book 12 | 13 | This updates the book so it is updated on push. 14 | 15 | // TODO! add this as a pre-commit hook 16 | 17 | ``` 18 | $ cd book 19 | $ mdbook build --dest-dir ../docs 20 | ``` 21 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Foreword](./1_foreword.md) 4 | - [Process](./2_0_process.md) 5 | - [Overview](./2_1_overview.md) 6 | - [Configuration](./2_2_config.md) 7 | - [Repository](./2_3_repository.md) 8 | - [Manifest](./2_4_manifest.md) 9 | - [API Extraction](./2_5_api_extraction.md) 10 | - [Comparator](./2_6_comparator.md) 11 | - [Diagnosis](./2_7_diagnosis.md) 12 | - [Next Version](./2_8_next_version.md) 13 | - [Book](./3_book.md) 14 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | This file makes sure that Github Pages doesn't process mdBook's output. 2 | -------------------------------------------------------------------------------- /docs/2_2_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Configuration - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Configuration

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/2_3_repository.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Repository - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Repository

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/2_4_manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Manifest - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Manifest

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/2_6_comparator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Comparator - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Comparator

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/2_7_diagnosis.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Diagnosis - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Diagnosis

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/2_8_next_version.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Next Version - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Next Version

141 |

WORK IN PROGRESS

142 | 143 |
144 | 145 | 155 |
156 |
157 | 158 | 166 | 167 |
168 | 169 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /docs/3_book.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Book - Guide to cargo-breaking Development 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 53 | 54 | 55 | 65 | 66 | 67 | 77 | 78 | 84 | 85 |
86 | 87 |
88 | 89 | 90 | 118 | 119 | 129 | 130 | 137 | 138 |
139 |
140 |

Book

141 |

Mdbook is needed to get running this book:

142 |
$ cargo install mdbook
143 | $ cd book
144 | $ mdbook serve --dest-dir ../docs
145 | 
146 |

Building the book

147 |

This updates the book so it is updated on push.

148 |

// TODO! add this as a pre-commit hook

149 |
$ cd book
150 | $ mdbook build --dest-dir ../docs
151 | 
152 | 153 |
154 | 155 | 162 |
163 |
164 | 165 | 170 | 171 |
172 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 54 | 55 | 56 | 66 | 67 | 68 | 78 | 79 | 85 | 86 |
87 | 88 |
89 | 90 | 91 | 119 | 120 | 130 | 131 | 138 | 139 |
140 |
141 |

Document not found (404)

142 |

This URL is invalid, sorry. Please use the navigation bar or search to continue.

143 | 144 |
145 | 146 | 150 |
151 |
152 | 153 | 155 | 156 |
157 | 158 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | book.cargo-breaking.com -------------------------------------------------------------------------------- /docs/FontAwesome/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/FontAwesome/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /docs/FontAwesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/FontAwesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/FontAwesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/FontAwesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/FontAwesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/FontAwesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/FontAwesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/FontAwesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docs/ayu-highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | Based off of the Ayu theme 3 | Original by Dempfi (https://github.com/dempfi/ayu) 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | overflow-x: auto; 9 | background: #191f26; 10 | color: #e6e1cf; 11 | padding: 0.5em; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-quote { 16 | color: #5c6773; 17 | font-style: italic; 18 | } 19 | 20 | .hljs-variable, 21 | .hljs-template-variable, 22 | .hljs-attribute, 23 | .hljs-attr, 24 | .hljs-regexp, 25 | .hljs-link, 26 | .hljs-selector-id, 27 | .hljs-selector-class { 28 | color: #ff7733; 29 | } 30 | 31 | .hljs-number, 32 | .hljs-meta, 33 | .hljs-builtin-name, 34 | .hljs-literal, 35 | .hljs-type, 36 | .hljs-params { 37 | color: #ffee99; 38 | } 39 | 40 | .hljs-string, 41 | .hljs-bullet { 42 | color: #b8cc52; 43 | } 44 | 45 | .hljs-title, 46 | .hljs-built_in, 47 | .hljs-section { 48 | color: #ffb454; 49 | } 50 | 51 | .hljs-keyword, 52 | .hljs-selector-tag, 53 | .hljs-symbol { 54 | color: #ff7733; 55 | } 56 | 57 | .hljs-name { 58 | color: #36a3d9; 59 | } 60 | 61 | .hljs-tag { 62 | color: #00568d; 63 | } 64 | 65 | .hljs-emphasis { 66 | font-style: italic; 67 | } 68 | 69 | .hljs-strong { 70 | font-weight: bold; 71 | } 72 | 73 | .hljs-addition { 74 | color: #91b362; 75 | } 76 | 77 | .hljs-deletion { 78 | color: #d96c75; 79 | } 80 | -------------------------------------------------------------------------------- /docs/clipboard.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v2.0.4 3 | * https://zenorocha.github.io/clipboard.js 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;n .buttons { 34 | z-index: 2; 35 | } 36 | 37 | a, a:visited, a:active, a:hover { 38 | color: #4183c4; 39 | text-decoration: none; 40 | } 41 | 42 | h1, h2, h3, h4, h5, h6 { 43 | page-break-inside: avoid; 44 | page-break-after: avoid; 45 | } 46 | 47 | pre, code { 48 | page-break-inside: avoid; 49 | white-space: pre-wrap; 50 | } 51 | 52 | .fa { 53 | display: none !important; 54 | } 55 | -------------------------------------------------------------------------------- /docs/css/variables.css: -------------------------------------------------------------------------------- 1 | 2 | /* Globals */ 3 | 4 | :root { 5 | --sidebar-width: 300px; 6 | --page-padding: 15px; 7 | --content-max-width: 750px; 8 | --menu-bar-height: 50px; 9 | } 10 | 11 | /* Themes */ 12 | 13 | .ayu { 14 | --bg: hsl(210, 25%, 8%); 15 | --fg: #c5c5c5; 16 | 17 | --sidebar-bg: #14191f; 18 | --sidebar-fg: #c8c9db; 19 | --sidebar-non-existant: #5c6773; 20 | --sidebar-active: #ffb454; 21 | --sidebar-spacer: #2d334f; 22 | 23 | --scrollbar: var(--sidebar-fg); 24 | 25 | --icons: #737480; 26 | --icons-hover: #b7b9cc; 27 | 28 | --links: #0096cf; 29 | 30 | --inline-code-color: #ffb454; 31 | 32 | --theme-popup-bg: #14191f; 33 | --theme-popup-border: #5c6773; 34 | --theme-hover: #191f26; 35 | 36 | --quote-bg: hsl(226, 15%, 17%); 37 | --quote-border: hsl(226, 15%, 22%); 38 | 39 | --table-border-color: hsl(210, 25%, 13%); 40 | --table-header-bg: hsl(210, 25%, 28%); 41 | --table-alternate-bg: hsl(210, 25%, 11%); 42 | 43 | --searchbar-border-color: #848484; 44 | --searchbar-bg: #424242; 45 | --searchbar-fg: #fff; 46 | --searchbar-shadow-color: #d4c89f; 47 | --searchresults-header-fg: #666; 48 | --searchresults-border-color: #888; 49 | --searchresults-li-bg: #252932; 50 | --search-mark-bg: #e3b171; 51 | } 52 | 53 | .coal { 54 | --bg: hsl(200, 7%, 8%); 55 | --fg: #98a3ad; 56 | 57 | --sidebar-bg: #292c2f; 58 | --sidebar-fg: #a1adb8; 59 | --sidebar-non-existant: #505254; 60 | --sidebar-active: #3473ad; 61 | --sidebar-spacer: #393939; 62 | 63 | --scrollbar: var(--sidebar-fg); 64 | 65 | --icons: #43484d; 66 | --icons-hover: #b3c0cc; 67 | 68 | --links: #2b79a2; 69 | 70 | --inline-code-color: #c5c8c6;; 71 | 72 | --theme-popup-bg: #141617; 73 | --theme-popup-border: #43484d; 74 | --theme-hover: #1f2124; 75 | 76 | --quote-bg: hsl(234, 21%, 18%); 77 | --quote-border: hsl(234, 21%, 23%); 78 | 79 | --table-border-color: hsl(200, 7%, 13%); 80 | --table-header-bg: hsl(200, 7%, 28%); 81 | --table-alternate-bg: hsl(200, 7%, 11%); 82 | 83 | --searchbar-border-color: #aaa; 84 | --searchbar-bg: #b7b7b7; 85 | --searchbar-fg: #000; 86 | --searchbar-shadow-color: #aaa; 87 | --searchresults-header-fg: #666; 88 | --searchresults-border-color: #98a3ad; 89 | --searchresults-li-bg: #2b2b2f; 90 | --search-mark-bg: #355c7d; 91 | } 92 | 93 | .light { 94 | --bg: hsl(0, 0%, 100%); 95 | --fg: hsl(0, 0%, 0%); 96 | 97 | --sidebar-bg: #fafafa; 98 | --sidebar-fg: hsl(0, 0%, 0%); 99 | --sidebar-non-existant: #aaaaaa; 100 | --sidebar-active: #1f1fff; 101 | --sidebar-spacer: #f4f4f4; 102 | 103 | --scrollbar: #8F8F8F; 104 | 105 | --icons: #747474; 106 | --icons-hover: #000000; 107 | 108 | --links: #20609f; 109 | 110 | --inline-code-color: #301900; 111 | 112 | --theme-popup-bg: #fafafa; 113 | --theme-popup-border: #cccccc; 114 | --theme-hover: #e6e6e6; 115 | 116 | --quote-bg: hsl(197, 37%, 96%); 117 | --quote-border: hsl(197, 37%, 91%); 118 | 119 | --table-border-color: hsl(0, 0%, 95%); 120 | --table-header-bg: hsl(0, 0%, 80%); 121 | --table-alternate-bg: hsl(0, 0%, 97%); 122 | 123 | --searchbar-border-color: #aaa; 124 | --searchbar-bg: #fafafa; 125 | --searchbar-fg: #000; 126 | --searchbar-shadow-color: #aaa; 127 | --searchresults-header-fg: #666; 128 | --searchresults-border-color: #888; 129 | --searchresults-li-bg: #e4f2fe; 130 | --search-mark-bg: #a2cff5; 131 | } 132 | 133 | .navy { 134 | --bg: hsl(226, 23%, 11%); 135 | --fg: #bcbdd0; 136 | 137 | --sidebar-bg: #282d3f; 138 | --sidebar-fg: #c8c9db; 139 | --sidebar-non-existant: #505274; 140 | --sidebar-active: #2b79a2; 141 | --sidebar-spacer: #2d334f; 142 | 143 | --scrollbar: var(--sidebar-fg); 144 | 145 | --icons: #737480; 146 | --icons-hover: #b7b9cc; 147 | 148 | --links: #2b79a2; 149 | 150 | --inline-code-color: #c5c8c6;; 151 | 152 | --theme-popup-bg: #161923; 153 | --theme-popup-border: #737480; 154 | --theme-hover: #282e40; 155 | 156 | --quote-bg: hsl(226, 15%, 17%); 157 | --quote-border: hsl(226, 15%, 22%); 158 | 159 | --table-border-color: hsl(226, 23%, 16%); 160 | --table-header-bg: hsl(226, 23%, 31%); 161 | --table-alternate-bg: hsl(226, 23%, 14%); 162 | 163 | --searchbar-border-color: #aaa; 164 | --searchbar-bg: #aeaec6; 165 | --searchbar-fg: #000; 166 | --searchbar-shadow-color: #aaa; 167 | --searchresults-header-fg: #5f5f71; 168 | --searchresults-border-color: #5c5c68; 169 | --searchresults-li-bg: #242430; 170 | --search-mark-bg: #a2cff5; 171 | } 172 | 173 | .rust { 174 | --bg: hsl(60, 9%, 87%); 175 | --fg: #262625; 176 | 177 | --sidebar-bg: #3b2e2a; 178 | --sidebar-fg: #c8c9db; 179 | --sidebar-non-existant: #505254; 180 | --sidebar-active: #e69f67; 181 | --sidebar-spacer: #45373a; 182 | 183 | --scrollbar: var(--sidebar-fg); 184 | 185 | --icons: #737480; 186 | --icons-hover: #262625; 187 | 188 | --links: #2b79a2; 189 | 190 | --inline-code-color: #6e6b5e; 191 | 192 | --theme-popup-bg: #e1e1db; 193 | --theme-popup-border: #b38f6b; 194 | --theme-hover: #99908a; 195 | 196 | --quote-bg: hsl(60, 5%, 75%); 197 | --quote-border: hsl(60, 5%, 70%); 198 | 199 | --table-border-color: hsl(60, 9%, 82%); 200 | --table-header-bg: #b3a497; 201 | --table-alternate-bg: hsl(60, 9%, 84%); 202 | 203 | --searchbar-border-color: #aaa; 204 | --searchbar-bg: #fafafa; 205 | --searchbar-fg: #000; 206 | --searchbar-shadow-color: #aaa; 207 | --searchresults-header-fg: #666; 208 | --searchresults-border-color: #888; 209 | --searchresults-li-bg: #dec2a2; 210 | --search-mark-bg: #e69f67; 211 | } 212 | 213 | @media (prefers-color-scheme: dark) { 214 | .light.no-js { 215 | --bg: hsl(200, 7%, 8%); 216 | --fg: #98a3ad; 217 | 218 | --sidebar-bg: #292c2f; 219 | --sidebar-fg: #a1adb8; 220 | --sidebar-non-existant: #505254; 221 | --sidebar-active: #3473ad; 222 | --sidebar-spacer: #393939; 223 | 224 | --scrollbar: var(--sidebar-fg); 225 | 226 | --icons: #43484d; 227 | --icons-hover: #b3c0cc; 228 | 229 | --links: #2b79a2; 230 | 231 | --inline-code-color: #c5c8c6;; 232 | 233 | --theme-popup-bg: #141617; 234 | --theme-popup-border: #43484d; 235 | --theme-hover: #1f2124; 236 | 237 | --quote-bg: hsl(234, 21%, 18%); 238 | --quote-border: hsl(234, 21%, 23%); 239 | 240 | --table-border-color: hsl(200, 7%, 13%); 241 | --table-header-bg: hsl(200, 7%, 28%); 242 | --table-alternate-bg: hsl(200, 7%, 11%); 243 | 244 | --searchbar-border-color: #aaa; 245 | --searchbar-bg: #b7b7b7; 246 | --searchbar-fg: #000; 247 | --searchbar-shadow-color: #aaa; 248 | --searchresults-header-fg: #666; 249 | --searchresults-border-color: #98a3ad; 250 | --searchresults-li-bg: #2b2b2f; 251 | --search-mark-bg: #355c7d; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/favicon.png -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/fonts/SOURCE-CODE-PRO-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /docs/fonts/fonts.css: -------------------------------------------------------------------------------- 1 | /* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */ 2 | /* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */ 3 | 4 | /* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 5 | @font-face { 6 | font-family: 'Open Sans'; 7 | font-style: normal; 8 | font-weight: 300; 9 | src: local('Open Sans Light'), local('OpenSans-Light'), 10 | url('open-sans-v17-all-charsets-300.woff2') format('woff2'); 11 | } 12 | 13 | /* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 14 | @font-face { 15 | font-family: 'Open Sans'; 16 | font-style: italic; 17 | font-weight: 300; 18 | src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), 19 | url('open-sans-v17-all-charsets-300italic.woff2') format('woff2'); 20 | } 21 | 22 | /* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 23 | @font-face { 24 | font-family: 'Open Sans'; 25 | font-style: normal; 26 | font-weight: 400; 27 | src: local('Open Sans Regular'), local('OpenSans-Regular'), 28 | url('open-sans-v17-all-charsets-regular.woff2') format('woff2'); 29 | } 30 | 31 | /* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 32 | @font-face { 33 | font-family: 'Open Sans'; 34 | font-style: italic; 35 | font-weight: 400; 36 | src: local('Open Sans Italic'), local('OpenSans-Italic'), 37 | url('open-sans-v17-all-charsets-italic.woff2') format('woff2'); 38 | } 39 | 40 | /* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 41 | @font-face { 42 | font-family: 'Open Sans'; 43 | font-style: normal; 44 | font-weight: 600; 45 | src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), 46 | url('open-sans-v17-all-charsets-600.woff2') format('woff2'); 47 | } 48 | 49 | /* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 50 | @font-face { 51 | font-family: 'Open Sans'; 52 | font-style: italic; 53 | font-weight: 600; 54 | src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), 55 | url('open-sans-v17-all-charsets-600italic.woff2') format('woff2'); 56 | } 57 | 58 | /* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 59 | @font-face { 60 | font-family: 'Open Sans'; 61 | font-style: normal; 62 | font-weight: 700; 63 | src: local('Open Sans Bold'), local('OpenSans-Bold'), 64 | url('open-sans-v17-all-charsets-700.woff2') format('woff2'); 65 | } 66 | 67 | /* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 68 | @font-face { 69 | font-family: 'Open Sans'; 70 | font-style: italic; 71 | font-weight: 700; 72 | src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), 73 | url('open-sans-v17-all-charsets-700italic.woff2') format('woff2'); 74 | } 75 | 76 | /* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 77 | @font-face { 78 | font-family: 'Open Sans'; 79 | font-style: normal; 80 | font-weight: 800; 81 | src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), 82 | url('open-sans-v17-all-charsets-800.woff2') format('woff2'); 83 | } 84 | 85 | /* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ 86 | @font-face { 87 | font-family: 'Open Sans'; 88 | font-style: italic; 89 | font-weight: 800; 90 | src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), 91 | url('open-sans-v17-all-charsets-800italic.woff2') format('woff2'); 92 | } 93 | 94 | /* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */ 95 | @font-face { 96 | font-family: 'Source Code Pro'; 97 | font-style: normal; 98 | font-weight: 500; 99 | src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2'); 100 | } 101 | -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-300.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-300italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-600.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-600italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-600italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-700.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-700italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-800.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-800italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-800italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/open-sans-v17-all-charsets-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/open-sans-v17-all-charsets-regular.woff2 -------------------------------------------------------------------------------- /docs/fonts/source-code-pro-v11-all-charsets-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iomentum/cargo-breaking/a4fa6f3c20890a73d89a0ef2ee7558d17049a95f/docs/fonts/source-code-pro-v11-all-charsets-500.woff2 -------------------------------------------------------------------------------- /docs/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | * An increased contrast highlighting scheme loosely based on the 3 | * "Base16 Atelier Dune Light" theme by Bram de Haan 4 | * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) 5 | * Original Base16 color scheme by Chris Kempson 6 | * (https://github.com/chriskempson/base16) 7 | */ 8 | 9 | /* Comment */ 10 | .hljs-comment, 11 | .hljs-quote { 12 | color: #575757; 13 | } 14 | 15 | /* Red */ 16 | .hljs-variable, 17 | .hljs-template-variable, 18 | .hljs-attribute, 19 | .hljs-tag, 20 | .hljs-name, 21 | .hljs-regexp, 22 | .hljs-link, 23 | .hljs-name, 24 | .hljs-selector-id, 25 | .hljs-selector-class { 26 | color: #d70025; 27 | } 28 | 29 | /* Orange */ 30 | .hljs-number, 31 | .hljs-meta, 32 | .hljs-built_in, 33 | .hljs-builtin-name, 34 | .hljs-literal, 35 | .hljs-type, 36 | .hljs-params { 37 | color: #b21e00; 38 | } 39 | 40 | /* Green */ 41 | .hljs-string, 42 | .hljs-symbol, 43 | .hljs-bullet { 44 | color: #008200; 45 | } 46 | 47 | /* Blue */ 48 | .hljs-title, 49 | .hljs-section { 50 | color: #0030f2; 51 | } 52 | 53 | /* Purple */ 54 | .hljs-keyword, 55 | .hljs-selector-tag { 56 | color: #9d00ec; 57 | } 58 | 59 | .hljs { 60 | display: block; 61 | overflow-x: auto; 62 | background: #f6f7f6; 63 | color: #000; 64 | padding: 0.5em; 65 | } 66 | 67 | .hljs-emphasis { 68 | font-style: italic; 69 | } 70 | 71 | .hljs-strong { 72 | font-weight: bold; 73 | } 74 | 75 | .hljs-addition { 76 | color: #22863a; 77 | background-color: #f0fff4; 78 | } 79 | 80 | .hljs-deletion { 81 | color: #b31d28; 82 | background-color: #ffeef0; 83 | } 84 | -------------------------------------------------------------------------------- /docs/tomorrow-night.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Night Theme */ 2 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 3 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 4 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 5 | 6 | /* Tomorrow Comment */ 7 | .hljs-comment { 8 | color: #969896; 9 | } 10 | 11 | /* Tomorrow Red */ 12 | .hljs-variable, 13 | .hljs-attribute, 14 | .hljs-tag, 15 | .hljs-regexp, 16 | .ruby .hljs-constant, 17 | .xml .hljs-tag .hljs-title, 18 | .xml .hljs-pi, 19 | .xml .hljs-doctype, 20 | .html .hljs-doctype, 21 | .css .hljs-id, 22 | .css .hljs-class, 23 | .css .hljs-pseudo { 24 | color: #cc6666; 25 | } 26 | 27 | /* Tomorrow Orange */ 28 | .hljs-number, 29 | .hljs-preprocessor, 30 | .hljs-pragma, 31 | .hljs-built_in, 32 | .hljs-literal, 33 | .hljs-params, 34 | .hljs-constant { 35 | color: #de935f; 36 | } 37 | 38 | /* Tomorrow Yellow */ 39 | .ruby .hljs-class .hljs-title, 40 | .css .hljs-rule .hljs-attribute { 41 | color: #f0c674; 42 | } 43 | 44 | /* Tomorrow Green */ 45 | .hljs-string, 46 | .hljs-value, 47 | .hljs-inheritance, 48 | .hljs-header, 49 | .hljs-name, 50 | .ruby .hljs-symbol, 51 | .xml .hljs-cdata { 52 | color: #b5bd68; 53 | } 54 | 55 | /* Tomorrow Aqua */ 56 | .hljs-title, 57 | .css .hljs-hexcolor { 58 | color: #8abeb7; 59 | } 60 | 61 | /* Tomorrow Blue */ 62 | .hljs-function, 63 | .python .hljs-decorator, 64 | .python .hljs-title, 65 | .ruby .hljs-function .hljs-title, 66 | .ruby .hljs-title .hljs-keyword, 67 | .perl .hljs-sub, 68 | .javascript .hljs-title, 69 | .coffeescript .hljs-title { 70 | color: #81a2be; 71 | } 72 | 73 | /* Tomorrow Purple */ 74 | .hljs-keyword, 75 | .javascript .hljs-function { 76 | color: #b294bb; 77 | } 78 | 79 | .hljs { 80 | display: block; 81 | overflow-x: auto; 82 | background: #1d1f21; 83 | color: #c5c8c6; 84 | padding: 0.5em; 85 | -webkit-text-size-adjust: none; 86 | } 87 | 88 | .coffeescript .javascript, 89 | .javascript .xml, 90 | .tex .hljs-formula, 91 | .xml .javascript, 92 | .xml .vbscript, 93 | .xml .css, 94 | .xml .hljs-cdata { 95 | opacity: 0.5; 96 | } 97 | 98 | .hljs-addition { 99 | color: #718c00; 100 | } 101 | 102 | .hljs-deletion { 103 | color: #c82829; 104 | } 105 | -------------------------------------------------------------------------------- /src/ast.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parse::{Parse, ParseStream, Result as ParseResult}, 3 | Error as SynError, File, 4 | }; 5 | 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone, Debug, PartialEq)] 9 | pub(crate) struct CrateAst(pub File); 10 | 11 | impl CrateAst { 12 | pub(crate) fn ast(&self) -> &File { 13 | &self.0 14 | } 15 | } 16 | 17 | impl FromStr for CrateAst { 18 | type Err = SynError; 19 | 20 | fn from_str(s: &str) -> Result { 21 | syn::parse_str(s).map(CrateAst) 22 | } 23 | } 24 | 25 | impl Parse for CrateAst { 26 | fn parse(input: ParseStream) -> ParseResult { 27 | Ok(CrateAst(input.parse()?)) 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn parse_simple_crate() { 37 | assert!(CrateAst::from_str("fn a() {}").is_ok()); 38 | } 39 | 40 | #[test] 41 | fn syntax_error_case() { 42 | assert!(CrateAst::from_str("fnn a() {}").is_err()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; 2 | 3 | pub(crate) struct ProgramConfig { 4 | pub comparaison_ref: String, 5 | } 6 | 7 | impl ProgramConfig { 8 | pub(crate) fn parse() -> ProgramConfig { 9 | let matches = App::new(crate_name!()) 10 | .version(crate_version!()) 11 | .author(crate_authors!()) 12 | .about(crate_description!()) 13 | .arg( 14 | Arg::with_name("crate_name") 15 | .required(false) 16 | ) 17 | .arg( 18 | Arg::with_name("against") 19 | .short("a") 20 | .help("Sets the git reference to compare the API against. Can be a tag, a branch name or a commit.") 21 | .takes_value(true) 22 | .required(false) 23 | .default_value("main") 24 | ).get_matches(); 25 | 26 | let comparaison_ref = matches.value_of("against").unwrap().to_owned(); 27 | 28 | ProgramConfig { comparaison_ref } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/diagnosis.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use syn::Ident; 4 | 5 | #[cfg(test)] 6 | use syn::{ 7 | parse::{Parse, ParseStream, Result as ParseResult}, 8 | Token, 9 | }; 10 | 11 | use crate::public_api::ItemPath; 12 | 13 | pub struct DiagnosisCollector { 14 | inner: Vec, 15 | } 16 | 17 | impl DiagnosisCollector { 18 | pub fn new() -> DiagnosisCollector { 19 | DiagnosisCollector { inner: Vec::new() } 20 | } 21 | 22 | pub(crate) fn add(&mut self, diagnosis_item: DiagnosisItem) { 23 | self.inner.push(diagnosis_item); 24 | } 25 | 26 | pub(crate) fn finalize(self) -> Vec { 27 | self.inner 28 | } 29 | } 30 | 31 | pub(crate) trait DiagnosticGenerator { 32 | fn removal_diagnosis(&self, path: &ItemPath, collector: &mut DiagnosisCollector) { 33 | collector.add(DiagnosisItem::removal(path.clone(), None)); 34 | } 35 | 36 | fn modification_diagnosis( 37 | &self, 38 | _other: &Self, 39 | path: &ItemPath, 40 | collector: &mut DiagnosisCollector, 41 | ) { 42 | collector.add(DiagnosisItem::modification(path.clone(), None)); 43 | } 44 | 45 | fn addition_diagnosis(&self, path: &ItemPath, collector: &mut DiagnosisCollector) { 46 | collector.add(DiagnosisItem::addition(path.clone(), None)); 47 | } 48 | } 49 | 50 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 51 | pub(crate) struct DiagnosisItem { 52 | kind: DiagnosisItemKind, 53 | path: ItemPath, 54 | trait_impl: Option, 55 | } 56 | 57 | impl DiagnosisItem { 58 | pub(crate) fn removal(path: ItemPath, trait_impl: Option) -> DiagnosisItem { 59 | DiagnosisItem { 60 | kind: DiagnosisItemKind::Removal, 61 | path, 62 | trait_impl, 63 | } 64 | } 65 | 66 | pub(crate) fn modification(path: ItemPath, trait_impl: Option) -> DiagnosisItem { 67 | DiagnosisItem { 68 | kind: DiagnosisItemKind::Modification, 69 | path, 70 | trait_impl, 71 | } 72 | } 73 | 74 | pub(crate) fn addition(path: ItemPath, trait_impl: Option) -> DiagnosisItem { 75 | DiagnosisItem { 76 | kind: DiagnosisItemKind::Addition, 77 | path, 78 | trait_impl, 79 | } 80 | } 81 | 82 | pub(crate) fn is_removal(&self) -> bool { 83 | self.kind == DiagnosisItemKind::Removal 84 | } 85 | 86 | pub(crate) fn is_modification(&self) -> bool { 87 | self.kind == DiagnosisItemKind::Modification 88 | } 89 | 90 | pub(crate) fn is_addition(&self) -> bool { 91 | self.kind == DiagnosisItemKind::Addition 92 | } 93 | } 94 | 95 | impl Display for DiagnosisItem { 96 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 97 | write!(f, "{} {}", self.kind, self.path)?; 98 | 99 | if let Some(trait_) = &self.trait_impl { 100 | write!(f, ": {}", trait_) 101 | } else { 102 | Ok(()) 103 | } 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | impl Parse for DiagnosisItem { 109 | fn parse(input: ParseStream) -> ParseResult { 110 | let kind = input.parse()?; 111 | let path = input.parse()?; 112 | 113 | let trait_impl = if input.peek(Token![:]) { 114 | input.parse::().unwrap(); 115 | input.parse::().unwrap(); 116 | 117 | Some(input.parse()?) 118 | } else { 119 | None 120 | }; 121 | 122 | Ok(DiagnosisItem { 123 | kind, 124 | path, 125 | trait_impl, 126 | }) 127 | } 128 | } 129 | 130 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] 131 | enum DiagnosisItemKind { 132 | Removal, 133 | Modification, 134 | Addition, 135 | } 136 | 137 | impl Display for DiagnosisItemKind { 138 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 139 | match self { 140 | DiagnosisItemKind::Removal => '-', 141 | DiagnosisItemKind::Modification => '≠', 142 | DiagnosisItemKind::Addition => '+', 143 | } 144 | .fmt(f) 145 | } 146 | } 147 | 148 | #[cfg(test)] 149 | impl Parse for DiagnosisItemKind { 150 | fn parse(input: ParseStream) -> ParseResult { 151 | if input.peek(Token![-]) { 152 | input.parse::().unwrap(); 153 | Ok(DiagnosisItemKind::Removal) 154 | } else if input.peek(Token![<]) { 155 | input.parse::().unwrap(); 156 | input.parse::]>().unwrap(); 157 | 158 | Ok(DiagnosisItemKind::Modification) 159 | } else if input.peek(Token![+]) { 160 | input.parse::().unwrap(); 161 | Ok(DiagnosisItemKind::Addition) 162 | } else { 163 | Err(input.error("Excepted `-`, `<>` or `+`")) 164 | } 165 | } 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | use syn::parse_quote; 171 | 172 | use super::*; 173 | 174 | #[test] 175 | fn display_implementation_standard_removal() { 176 | let diag: DiagnosisItem = parse_quote! { 177 | - foo::baz::Bar 178 | }; 179 | 180 | assert_eq!(diag.to_string(), "- foo::baz::Bar"); 181 | } 182 | 183 | #[test] 184 | fn display_implementation_standard_modification() { 185 | let diag: DiagnosisItem = parse_quote! { 186 | <> foo::Bar 187 | }; 188 | 189 | assert_eq!(diag.to_string(), "≠ foo::Bar"); 190 | } 191 | 192 | #[test] 193 | fn display_implementation_standard_addition() { 194 | let diag: DiagnosisItem = parse_quote! { 195 | + foo::bar::Baz 196 | }; 197 | 198 | assert_eq!(diag.to_string(), "+ foo::bar::Baz"); 199 | } 200 | 201 | #[test] 202 | fn display_implementation_trait_impl() { 203 | let diag: DiagnosisItem = parse_quote! { 204 | <> foo::bar::Baz: impl Foo 205 | }; 206 | 207 | assert_eq!(diag.to_string(), "≠ foo::bar::Baz: Foo"); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/glue.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{Display, Formatter, Result as FmtResult}, 4 | process::Command, 5 | str::FromStr, 6 | }; 7 | 8 | use anyhow::{bail, Context, Result as AnyResult}; 9 | use syn::Error as SynError; 10 | 11 | use crate::{ast::CrateAst, comparator::ApiComparator, public_api::PublicApi}; 12 | 13 | pub(crate) fn extract_api() -> AnyResult { 14 | let output = Command::new("cargo") 15 | .arg("+nightly") 16 | .arg("rustc") 17 | .arg("--lib") 18 | .arg("--") 19 | .args(&["-Z", "unpretty=expanded"]) 20 | .args(&["-Z", "unpretty=everybody_loops"]) 21 | .arg("--emit=mir") 22 | .output() 23 | .context("Failed to run `cargo rustc`")?; 24 | 25 | if !output.status.success() { 26 | let stderr = String::from_utf8(output.stderr) 27 | .map_err(|_| InvalidRustcOutputEncoding) 28 | .context("Failed to get rustc error message")?; 29 | bail!(stderr); 30 | } 31 | 32 | let expanded_code = String::from_utf8(output.stdout) 33 | .map_err(|_| InvalidRustcOutputEncoding) 34 | .context("Failed to get rustc-expanded crate code")?; 35 | 36 | let ast = CrateAst::from_str(&expanded_code) 37 | .map_err(InvalidRustcAst) 38 | .context("Failed to parse rustc-provided crate AST")?; 39 | 40 | let api = PublicApi::from_ast(&ast); 41 | 42 | Ok(api) 43 | } 44 | 45 | #[derive(Clone, Copy, Debug, PartialEq)] 46 | struct InvalidRustcOutputEncoding; 47 | 48 | impl Display for InvalidRustcOutputEncoding { 49 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 50 | write!(f, "rustc yielded non-UTF-8 output") 51 | } 52 | } 53 | 54 | impl Error for InvalidRustcOutputEncoding {} 55 | 56 | #[derive(Clone, Debug)] 57 | struct InvalidRustcAst(SynError); 58 | 59 | impl Display for InvalidRustcAst { 60 | fn fmt(&self, f: &mut Formatter) -> FmtResult { 61 | write!(f, "rustc yielded an invalid program: {}", self.0) 62 | } 63 | } 64 | 65 | impl Error for InvalidRustcAst {} 66 | 67 | pub fn compare(prev: &str, curr: &str) -> AnyResult { 68 | let prev_ast = CrateAst::from_str(prev).context("Failed to parse code for previous version")?; 69 | let curr_ast = CrateAst::from_str(curr).context("Failed to parse code for current version")?; 70 | 71 | let prev_api = PublicApi::from_ast(&prev_ast); 72 | let curr_api = PublicApi::from_ast(&curr_ast); 73 | 74 | Ok(ApiComparator::new(prev_api, curr_api)) 75 | } 76 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ast; 2 | mod cli; 3 | mod comparator; 4 | mod diagnosis; 5 | mod git; 6 | mod glue; 7 | mod manifest; 8 | mod public_api; 9 | 10 | use anyhow::{Context, Result as AnyResult}; 11 | pub use comparator::ApiCompatibilityDiagnostics; 12 | pub use glue::compare; 13 | 14 | use crate::{ 15 | comparator::ApiComparator, 16 | git::{CrateRepo, GitBackend}, 17 | }; 18 | 19 | pub fn run() -> AnyResult<()> { 20 | let config = cli::ProgramConfig::parse(); 21 | 22 | let mut repo = CrateRepo::current().context("Failed to fetch repository data")?; 23 | 24 | let version = manifest::get_crate_version().context("Failed to get crate version")?; 25 | 26 | let current_api = glue::extract_api().context("Failed to get crate API")?; 27 | 28 | let previous_api = repo.run_in(config.comparaison_ref.as_str(), || { 29 | glue::extract_api().context("Failed to get crate API") 30 | })??; 31 | 32 | let api_comparator = ApiComparator::new(previous_api, current_api); 33 | 34 | let diagnosis = api_comparator.run(); 35 | 36 | if !diagnosis.is_empty() { 37 | println!("{}", diagnosis); 38 | } 39 | 40 | let next_version = diagnosis.guess_next_version(version); 41 | println!("Next version is: {}", next_version); 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result as AnyResult; 2 | 3 | fn main() -> AnyResult<()> { 4 | cargo_breaking::run() 5 | } 6 | -------------------------------------------------------------------------------- /src/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{bail, Context, Result as AnyResult}; 4 | use cargo_toml::Manifest; 5 | use semver::Version; 6 | 7 | pub(crate) fn get_crate_version() -> AnyResult { 8 | let m = load_manifest()?; 9 | get_version_from_manifest(&m).context("Failed to get version from crate manifest") 10 | } 11 | 12 | fn load_manifest() -> AnyResult { 13 | let p = Path::new("Cargo.toml"); 14 | Manifest::from_path(p).context("Failed to load crate manifest") 15 | } 16 | 17 | fn get_version_from_manifest(m: &Manifest) -> AnyResult { 18 | let unparsed_version = match &m.package { 19 | Some(package) => &package.version, 20 | None => bail!("Expected a package, found a workspace"), 21 | }; 22 | 23 | Version::parse(unparsed_version.as_str()).context("Failed to parser version string") 24 | } 25 | -------------------------------------------------------------------------------- /src/public_api/functions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{ 4 | visit::{self, Visit}, 5 | Ident, ItemFn, ItemMod, Signature, Visibility, 6 | }; 7 | 8 | #[cfg(test)] 9 | use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult}; 10 | 11 | use crate::diagnosis::DiagnosticGenerator; 12 | 13 | use super::{ItemKind, ItemPath}; 14 | 15 | #[derive(Clone, Debug, PartialEq)] 16 | pub(crate) struct FnVisitor { 17 | items: HashMap, 18 | path: Vec, 19 | } 20 | 21 | impl FnVisitor { 22 | pub(crate) fn new(items: HashMap) -> FnVisitor { 23 | let path = Vec::new(); 24 | 25 | FnVisitor { items, path } 26 | } 27 | 28 | pub(crate) fn items(self) -> HashMap { 29 | self.items 30 | } 31 | 32 | fn add_path_segment(&mut self, segment: Ident) { 33 | self.path.push(segment); 34 | } 35 | 36 | fn remove_path_segment(&mut self) { 37 | self.path.pop().unwrap(); 38 | } 39 | 40 | fn add_fn(&mut self, path: ItemPath, fn_: FnPrototype) { 41 | let tmp = self.items.insert(path, fn_.into()); 42 | 43 | assert!(tmp.is_none(), "Duplicate item definition"); 44 | } 45 | } 46 | 47 | impl<'ast> Visit<'ast> for FnVisitor { 48 | fn visit_item_mod(&mut self, mod_: &'ast ItemMod) { 49 | self.add_path_segment(mod_.ident.clone()); 50 | visit::visit_item_mod(self, mod_); 51 | self.remove_path_segment(); 52 | } 53 | 54 | fn visit_item_fn(&mut self, fn_: &'ast ItemFn) { 55 | if !matches!(fn_.vis, Visibility::Public(_)) { 56 | return; 57 | } 58 | 59 | let path = ItemPath::new(self.path.clone(), fn_.sig.ident.clone()); 60 | let fn_ = FnPrototype::new(fn_.sig.clone()); 61 | 62 | self.add_fn(path, fn_); 63 | } 64 | } 65 | 66 | #[derive(Clone, Debug, PartialEq)] 67 | pub(crate) struct FnPrototype { 68 | sig: Signature, 69 | } 70 | 71 | impl FnPrototype { 72 | fn new(mut sig: Signature) -> FnPrototype { 73 | if let Some(last) = sig.inputs.pop() { 74 | sig.inputs.push(last.value().clone()); 75 | } 76 | FnPrototype { sig } 77 | } 78 | } 79 | 80 | impl DiagnosticGenerator for FnPrototype {} 81 | 82 | #[cfg(test)] 83 | impl Parse for FnPrototype { 84 | fn parse(input: ParseStream) -> ParseResult { 85 | let vis = input.parse()?; 86 | 87 | if !matches!(vis, Visibility::Public(_)) { 88 | let err_span = input.span(); 89 | return Err(ParseError::new( 90 | err_span, 91 | "Found non-public function in test code", 92 | )); 93 | } 94 | 95 | let sig = input.parse()?; 96 | Ok(FnPrototype { sig }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/public_api/methods.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{ 4 | visit::{self, Visit}, 5 | AngleBracketedGenericArguments, Generics, Ident, ImplItemMethod, ItemImpl, ItemMod, Signature, 6 | Visibility, 7 | }; 8 | 9 | #[cfg(test)] 10 | use syn::{ 11 | parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult}, 12 | spanned::Spanned, 13 | }; 14 | 15 | use crate::diagnosis::DiagnosticGenerator; 16 | 17 | use super::{imports::PathResolver, utils, ItemKind, ItemPath}; 18 | 19 | #[derive(Clone, Debug, PartialEq)] 20 | pub(crate) struct MethodVisitor<'a> { 21 | items: HashMap, 22 | path: Vec, 23 | resolver: &'a PathResolver, 24 | } 25 | 26 | impl<'a> MethodVisitor<'a> { 27 | pub(crate) fn new( 28 | types: HashMap, 29 | resolver: &'a PathResolver, 30 | ) -> MethodVisitor<'a> { 31 | let items = types; 32 | let path = Vec::new(); 33 | 34 | MethodVisitor { 35 | items, 36 | path, 37 | resolver, 38 | } 39 | } 40 | 41 | pub(crate) fn items(self) -> HashMap { 42 | self.items 43 | } 44 | 45 | fn add_path_segment(&mut self, segment: Ident) { 46 | self.path.push(segment); 47 | } 48 | 49 | fn remove_path_segment(&mut self) { 50 | self.path.pop().unwrap(); 51 | } 52 | } 53 | 54 | impl<'a, 'ast> Visit<'ast> for MethodVisitor<'a> { 55 | fn visit_item_mod(&mut self, mod_: &'ast ItemMod) { 56 | self.add_path_segment(mod_.ident.clone()); 57 | visit::visit_item_mod(self, mod_); 58 | self.remove_path_segment(); 59 | } 60 | 61 | fn visit_item_impl(&mut self, impl_: &'ast ItemImpl) { 62 | if impl_.trait_.is_some() { 63 | return; 64 | } 65 | 66 | let (type_path, generic_args) = 67 | match utils::extract_name_and_generic_args(impl_.self_ty.as_ref()) { 68 | Some((name, generic_args)) => (name, generic_args.cloned()), 69 | // TODO: handle non-trivial paths 70 | None => return, 71 | }; 72 | 73 | let resolved_type_path = match self.resolver.resolve(self.path.as_slice(), type_path) { 74 | Some(resolved) => resolved, 75 | None => return, 76 | }; 77 | 78 | let generic_params = &impl_.generics; 79 | 80 | let mut impl_block_visitor = ImplBlockVisitor { 81 | items: &mut self.items, 82 | path: resolved_type_path, 83 | parent_generic_params: generic_params, 84 | parent_generic_args: &generic_args, 85 | }; 86 | impl_block_visitor.visit_item_impl(impl_); 87 | } 88 | } 89 | 90 | #[derive(Debug, PartialEq)] 91 | struct ImplBlockVisitor<'a> { 92 | items: &'a mut HashMap, 93 | path: &'a [Ident], 94 | parent_generic_params: &'a Generics, 95 | parent_generic_args: &'a Option, 96 | } 97 | 98 | impl<'a> ImplBlockVisitor<'a> { 99 | fn add_method(&mut self, path: ItemPath, method: MethodMetadata) { 100 | let tmp = self.items.insert(path, method.into()); 101 | 102 | assert!(tmp.is_none(), "Duplicate item definition"); 103 | } 104 | } 105 | 106 | impl<'a, 'ast> Visit<'ast> for ImplBlockVisitor<'a> { 107 | fn visit_impl_item_method(&mut self, method: &'ast ImplItemMethod) { 108 | if !matches!(method.vis, Visibility::Public(_)) { 109 | return; 110 | } 111 | 112 | let path = ItemPath::new(self.path.to_owned(), method.sig.ident.clone()); 113 | let method = MethodMetadata::new( 114 | method.sig.clone(), 115 | self.parent_generic_params.clone(), 116 | self.parent_generic_args.clone(), 117 | ); 118 | 119 | self.add_method(path, method); 120 | } 121 | } 122 | 123 | #[derive(Clone, Debug, PartialEq)] 124 | pub(crate) struct MethodMetadata { 125 | signature: Signature, 126 | parent_generic_params: Generics, 127 | parent_generic_args: Option, 128 | } 129 | 130 | impl MethodMetadata { 131 | fn new( 132 | signature: Signature, 133 | parent_generic_params: Generics, 134 | parent_generic_args: Option, 135 | ) -> MethodMetadata { 136 | MethodMetadata { 137 | signature, 138 | parent_generic_params, 139 | parent_generic_args, 140 | } 141 | } 142 | } 143 | 144 | impl DiagnosticGenerator for MethodMetadata {} 145 | 146 | #[cfg(test)] 147 | impl Parse for MethodMetadata { 148 | fn parse(input: ParseStream) -> ParseResult { 149 | let impl_block = input.parse::()?; 150 | 151 | let parent_generc_params = &impl_block.generics; 152 | let (_, parent_generic_arguments) = 153 | utils::extract_name_and_generic_args(&impl_block.self_ty).unwrap(); 154 | 155 | let inner_item = match impl_block.items.len() { 156 | 1 => impl_block.items.last().unwrap(), 157 | _ => { 158 | return Err(ParseError::new( 159 | impl_block.span(), 160 | "Excepted a single function", 161 | )) 162 | } 163 | }; 164 | 165 | let method = match inner_item { 166 | syn::ImplItem::Method(m) => m, 167 | _ => return Err(ParseError::new(inner_item.span(), "Excepted a method")), 168 | }; 169 | 170 | let sig = &method.sig; 171 | 172 | Ok(MethodMetadata::new( 173 | sig.clone(), 174 | parent_generc_params.clone(), 175 | parent_generic_arguments.cloned(), 176 | )) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/public_api/trait_defs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{ 4 | punctuated::Punctuated, 5 | token::Add, 6 | visit::{self, Visit}, 7 | Generics, Ident, ItemMod, ItemTrait, TraitItem, TraitItemConst, TraitItemMethod, TraitItemType, 8 | TypeParamBound, Visibility, 9 | }; 10 | 11 | #[cfg(test)] 12 | use syn::parse::{Parse, ParseStream, Result as ParseResult}; 13 | 14 | use crate::diagnosis::{DiagnosisCollector, DiagnosisItem, DiagnosticGenerator}; 15 | 16 | use super::{imports::PathResolver, ItemKind, ItemPath}; 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub(crate) struct TraitDefVisitor<'a> { 20 | items: HashMap, 21 | path: Vec, 22 | resolver: &'a PathResolver, 23 | } 24 | 25 | impl<'a> TraitDefVisitor<'a> { 26 | pub(crate) fn new( 27 | items: HashMap, 28 | resolver: &'a PathResolver, 29 | ) -> TraitDefVisitor<'a> { 30 | let path = Vec::new(); 31 | TraitDefVisitor { 32 | items, 33 | path, 34 | resolver, 35 | } 36 | } 37 | 38 | pub(crate) fn items(self) -> HashMap { 39 | self.items 40 | } 41 | 42 | pub(crate) fn add_trait_def(&mut self, path: ItemPath, metadata: TraitDefMetadata) { 43 | let tmp = self.items.insert(path, metadata.into()); 44 | assert!(tmp.is_none(), "Duplicate item definition"); 45 | } 46 | } 47 | 48 | impl<'a, 'ast> Visit<'ast> for TraitDefVisitor<'a> { 49 | fn visit_item_trait(&mut self, i: &'ast ItemTrait) { 50 | if !matches!(i.vis, Visibility::Public(_)) { 51 | return; 52 | } 53 | 54 | let path = ItemPath::new(self.path.clone(), i.ident.clone()); 55 | let metadata = extract_def_trait_metadata(i); 56 | 57 | self.add_trait_def(path, metadata); 58 | } 59 | 60 | fn visit_item_mod(&mut self, i: &'ast ItemMod) { 61 | if !matches!(i.vis, Visibility::Public(_)) { 62 | return; 63 | } 64 | 65 | self.path.push(i.ident.clone()); 66 | visit::visit_item_mod(self, i); 67 | self.path.pop().unwrap(); 68 | } 69 | } 70 | 71 | fn extract_def_trait_metadata(i: &ItemTrait) -> TraitDefMetadata { 72 | let generics = i.generics.clone(); 73 | let supertraits = i.supertraits.clone(); 74 | 75 | let (mut consts, mut methods, mut types) = (Vec::new(), Vec::new(), Vec::new()); 76 | 77 | i.items.iter().for_each(|item| match item { 78 | TraitItem::Const(c) => consts.push(c.clone()), 79 | TraitItem::Method(m) => methods.push(m.clone()), 80 | TraitItem::Type(t) => types.push(t.clone()), 81 | other => panic!("Found unexcepted trait item: `{:?}`", other), 82 | }); 83 | 84 | TraitDefMetadata { 85 | generics, 86 | supertraits, 87 | consts, 88 | methods, 89 | types, 90 | } 91 | } 92 | 93 | #[derive(Clone, Debug, PartialEq)] 94 | pub(crate) struct TraitDefMetadata { 95 | generics: Generics, 96 | supertraits: Punctuated, 97 | consts: Vec, 98 | methods: Vec, 99 | types: Vec, 100 | } 101 | 102 | impl From for ItemKind { 103 | fn from(metadata: TraitDefMetadata) -> ItemKind { 104 | ItemKind::TraitDef(metadata) 105 | } 106 | } 107 | 108 | impl DiagnosticGenerator for TraitDefMetadata { 109 | fn modification_diagnosis( 110 | &self, 111 | other: &Self, 112 | path: &ItemPath, 113 | collector: &mut DiagnosisCollector, 114 | ) { 115 | if self.generics != other.generics || self.supertraits != other.supertraits { 116 | collector.add(DiagnosisItem::modification(path.clone(), None)); 117 | } 118 | 119 | diagnosis_for_nameable( 120 | self.consts.as_slice(), 121 | other.consts.as_slice(), 122 | path, 123 | collector, 124 | ); 125 | 126 | diagnosis_for_nameable( 127 | self.methods.as_slice(), 128 | other.methods.as_slice(), 129 | path, 130 | collector, 131 | ); 132 | 133 | diagnosis_for_nameable( 134 | self.types.as_slice(), 135 | other.types.as_slice(), 136 | path, 137 | collector, 138 | ); 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | impl Parse for TraitDefMetadata { 144 | fn parse(input: ParseStream) -> ParseResult { 145 | input 146 | .parse() 147 | .map(|trait_def| extract_def_trait_metadata(&trait_def)) 148 | } 149 | } 150 | 151 | fn diagnosis_for_nameable( 152 | left: &[Item], 153 | right: &[Item], 154 | path: &ItemPath, 155 | collector: &mut DiagnosisCollector, 156 | ) where 157 | Item: Nameable + PartialEq, 158 | { 159 | for left_item in left { 160 | let left_item_name = left_item.name(); 161 | 162 | match Item::find_named(right, left_item_name) { 163 | Some(right_item) if left_item == right_item => {} 164 | 165 | altered => { 166 | let path = ItemPath::extend(path.clone(), left_item_name.clone()); 167 | let diagnostic_creator = if altered.is_some() { 168 | DiagnosisItem::modification 169 | } else { 170 | DiagnosisItem::removal 171 | }; 172 | 173 | let diagnosis = diagnostic_creator(path, None); 174 | collector.add(diagnosis); 175 | } 176 | } 177 | } 178 | 179 | for right_item in right { 180 | let right_item_name = right_item.name(); 181 | 182 | if Item::find_named(left, right_item_name).is_none() { 183 | let path = ItemPath::extend(path.clone(), right_item_name.clone()); 184 | let diagnosis = DiagnosisItem::addition(path, None); 185 | collector.add(diagnosis) 186 | } 187 | } 188 | } 189 | 190 | trait Nameable: Sized { 191 | fn name(&self) -> &Ident; 192 | 193 | fn find_named<'a>(items: &'a [Self], name: &Ident) -> Option<&'a Self> { 194 | items.iter().find(|item| item.name() == name) 195 | } 196 | } 197 | 198 | impl Nameable for TraitItem { 199 | fn name(&self) -> &Ident { 200 | match self { 201 | TraitItem::Const(c) => &c.ident, 202 | TraitItem::Method(m) => &m.sig.ident, 203 | TraitItem::Type(t) => &t.ident, 204 | other => panic!("Found illegal trait item:\n{:#?}", other), 205 | } 206 | } 207 | } 208 | 209 | impl Nameable for TraitItemConst { 210 | fn name(&self) -> &Ident { 211 | &self.ident 212 | } 213 | } 214 | 215 | impl Nameable for TraitItemMethod { 216 | fn name(&self) -> &Ident { 217 | &self.sig.ident 218 | } 219 | } 220 | 221 | impl Nameable for TraitItemType { 222 | fn name(&self) -> &Ident { 223 | &self.ident 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/public_api/trait_impls.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{ 4 | visit::{self, Visit}, 5 | AngleBracketedGenericArguments, Generics, Ident, ImplItemConst, ImplItemType, ItemImpl, 6 | ItemMod, 7 | }; 8 | 9 | #[cfg(test)] 10 | use syn::parse::{Parse, ParseStream, Result as ParseResult}; 11 | 12 | use crate::{ 13 | diagnosis::{DiagnosisCollector, DiagnosisItem, DiagnosticGenerator}, 14 | public_api::utils, 15 | }; 16 | 17 | #[cfg(test)] 18 | use crate::ast::CrateAst; 19 | 20 | use super::{imports::PathResolver, ItemKind, ItemPath}; 21 | 22 | #[derive(Clone, Debug, PartialEq)] 23 | pub(crate) struct TraitImplVisitor<'a> { 24 | items: HashMap, 25 | path: Vec, 26 | resolver: &'a PathResolver, 27 | } 28 | 29 | impl<'a> TraitImplVisitor<'a> { 30 | pub(crate) fn new( 31 | items: HashMap, 32 | resolver: &'a PathResolver, 33 | ) -> TraitImplVisitor<'a> { 34 | let path = Vec::new(); 35 | TraitImplVisitor { 36 | items, 37 | path, 38 | resolver, 39 | } 40 | } 41 | 42 | pub(crate) fn items(self) -> HashMap { 43 | self.items 44 | } 45 | 46 | fn add_path_segment(&mut self, segment: Ident) { 47 | self.path.push(segment); 48 | } 49 | 50 | fn remove_path_segment(&mut self) { 51 | self.path.pop().unwrap(); 52 | } 53 | 54 | fn add_trait_impl(&mut self, type_path: &ItemPath, impl_: TraitImplMetadata) { 55 | let type_ = self 56 | .items 57 | .get_mut(type_path) 58 | .expect("Type not found") 59 | .as_type_mut() 60 | .expect("Can't impl a trait for a non-type item"); 61 | 62 | type_.add_trait_impl(impl_); 63 | } 64 | } 65 | 66 | impl<'a, 'ast> Visit<'ast> for TraitImplVisitor<'a> { 67 | fn visit_item_mod(&mut self, mod_: &'ast ItemMod) { 68 | self.add_path_segment(mod_.ident.clone()); 69 | visit::visit_item_mod(self, mod_); 70 | self.remove_path_segment(); 71 | } 72 | 73 | fn visit_item_impl(&mut self, impl_: &'ast ItemImpl) { 74 | let (type_name, trait_impl_metadata) = 75 | match extract_impl_trait_metadata(impl_, self.resolver, self.path.as_slice()) { 76 | Some(value) => value, 77 | None => return, 78 | }; 79 | 80 | self.add_trait_impl( 81 | &ItemPath::concat_both(self.path.clone(), type_name.to_owned()), 82 | trait_impl_metadata, 83 | ); 84 | } 85 | } 86 | 87 | fn extract_impl_trait_metadata<'a>( 88 | impl_: &ItemImpl, 89 | resolver: &'a PathResolver, 90 | current_path: &[Ident], 91 | ) -> Option<(&'a [Ident], TraitImplMetadata)> { 92 | let trait_path = match &impl_.trait_ { 93 | Some((_, trait_path, _)) => trait_path, 94 | None => return None, 95 | }; 96 | 97 | let (trait_name, trait_generic_args) = 98 | utils::extract_name_and_generic_args_from_path(trait_path)?; 99 | 100 | let trait_name = trait_name.clone(); 101 | let trait_generic_args = trait_generic_args.cloned(); 102 | 103 | let (type_path, type_generic_args) = 104 | utils::extract_name_and_generic_args(impl_.self_ty.as_ref())?; 105 | 106 | let resolved_path = resolver.resolve(current_path, type_path)?; 107 | let type_generic_args = type_generic_args.cloned(); 108 | 109 | let mut consts = Vec::new(); 110 | let mut types = Vec::new(); 111 | 112 | for item in &impl_.items { 113 | match item { 114 | syn::ImplItem::Const(c) => consts.push(c.clone()), 115 | syn::ImplItem::Type(t) => types.push(t.clone()), 116 | _ => {} 117 | } 118 | } 119 | 120 | let generic_parameters = impl_.generics.clone(); 121 | 122 | let trait_impl_metadata = TraitImplMetadata { 123 | trait_name, 124 | generic_parameters, 125 | trait_generic_args, 126 | type_generic_args, 127 | consts, 128 | types, 129 | }; 130 | 131 | Some((resolved_path, trait_impl_metadata)) 132 | } 133 | 134 | #[derive(Clone, Debug, PartialEq)] 135 | pub(crate) struct TraitImplMetadata { 136 | trait_name: Ident, 137 | generic_parameters: Generics, 138 | trait_generic_args: Option, 139 | type_generic_args: Option, 140 | 141 | consts: Vec, 142 | types: Vec, 143 | } 144 | 145 | impl TraitImplMetadata { 146 | pub(crate) fn trait_name(&self) -> &Ident { 147 | &self.trait_name 148 | } 149 | } 150 | 151 | impl DiagnosticGenerator for TraitImplMetadata { 152 | fn removal_diagnosis(&self, path: &ItemPath, collector: &mut DiagnosisCollector) { 153 | collector.add(DiagnosisItem::removal( 154 | path.clone(), 155 | Some(self.trait_name.clone()), 156 | )); 157 | } 158 | 159 | fn modification_diagnosis( 160 | &self, 161 | _other: &Self, 162 | path: &ItemPath, 163 | collector: &mut DiagnosisCollector, 164 | ) { 165 | collector.add(DiagnosisItem::modification( 166 | path.clone(), 167 | Some(self.trait_name.clone()), 168 | )); 169 | } 170 | 171 | fn addition_diagnosis(&self, path: &ItemPath, collector: &mut DiagnosisCollector) { 172 | collector.add(DiagnosisItem::addition( 173 | path.clone(), 174 | Some(self.trait_name.clone()), 175 | )); 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | impl Parse for TraitImplMetadata { 181 | fn parse(input: ParseStream) -> ParseResult { 182 | let impl_ = input.fork().parse::()?; 183 | let ast = input.parse::()?; 184 | 185 | let resolver = PathResolver::new(&ast); 186 | 187 | match extract_impl_trait_metadata(&impl_, &resolver, &[]) { 188 | Some((_, metadata)) => Ok(metadata), 189 | None => Err(input.error("Failed to parse trait implementation metadata")), 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/public_api/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use syn::{ 4 | punctuated::Punctuated, 5 | token::Comma, 6 | visit::{self, Visit}, 7 | Field, Fields, FieldsNamed, FieldsUnnamed, Generics, Ident, ItemEnum, ItemMod, ItemStruct, 8 | Variant, Visibility, 9 | }; 10 | 11 | use tap::Conv; 12 | 13 | #[cfg(test)] 14 | use syn::parse::{Parse, ParseStream, Result as ParseResult}; 15 | 16 | use crate::diagnosis::{DiagnosisCollector, DiagnosisItem, DiagnosticGenerator}; 17 | 18 | use super::{trait_impls::TraitImplMetadata, ItemKind, ItemPath}; 19 | 20 | #[derive(Clone, Debug, Default, PartialEq)] 21 | pub(crate) struct TypeVisitor { 22 | types: HashMap, 23 | path: Vec, 24 | } 25 | 26 | impl TypeVisitor { 27 | pub(crate) fn new() -> TypeVisitor { 28 | TypeVisitor::default() 29 | } 30 | 31 | pub(crate) fn types(self) -> HashMap { 32 | self.types 33 | } 34 | 35 | fn add_path_segment(&mut self, segment: Ident) { 36 | self.path.push(segment); 37 | } 38 | 39 | fn remove_path_segment(&mut self) { 40 | self.path.pop().unwrap(); 41 | } 42 | 43 | fn add_type(&mut self, path: ItemPath, kind: ItemKind) { 44 | let tmp = self.types.insert(path, kind); 45 | assert!(tmp.is_none(), "Duplicate item definition"); 46 | } 47 | } 48 | 49 | impl<'ast> Visit<'ast> for TypeVisitor { 50 | fn visit_item_mod(&mut self, mod_: &'ast ItemMod) { 51 | if matches!(mod_.vis, Visibility::Public(_)) { 52 | self.add_path_segment(mod_.ident.clone()); 53 | visit::visit_item_mod(self, mod_); 54 | self.remove_path_segment(); 55 | } 56 | } 57 | 58 | fn visit_item_struct(&mut self, i: &'ast ItemStruct) { 59 | if !matches!(i.vis, Visibility::Public(_)) { 60 | return; 61 | } 62 | 63 | let k = ItemPath::new(self.path.clone(), i.ident.clone()); 64 | let v = StructMetadata::new(i.generics.clone(), i.fields.clone()) 65 | .conv::() 66 | .into(); 67 | 68 | self.add_type(k, v); 69 | } 70 | 71 | fn visit_item_enum(&mut self, i: &'ast ItemEnum) { 72 | if !matches!(i.vis, Visibility::Public(_)) { 73 | return; 74 | } 75 | 76 | let k = ItemPath::new(self.path.clone(), i.ident.clone()); 77 | let v = EnumMetadata::new(i.generics.clone(), i.variants.clone()) 78 | .conv::() 79 | .into(); 80 | 81 | self.add_type(k, v); 82 | } 83 | } 84 | 85 | #[derive(Clone, Debug, PartialEq)] 86 | pub(crate) struct TypeMetadata { 87 | inner: InnerTypeMetadata, 88 | traits: Vec, 89 | } 90 | 91 | #[cfg(test)] 92 | impl TypeMetadata { 93 | pub(crate) fn traits(&self) -> &[TraitImplMetadata] { 94 | &self.traits 95 | } 96 | } 97 | 98 | impl TypeMetadata { 99 | fn new(inner: InnerTypeMetadata) -> TypeMetadata { 100 | TypeMetadata { 101 | inner, 102 | traits: Vec::new(), 103 | } 104 | } 105 | 106 | pub(crate) fn add_trait_impl(&mut self, impl_: TraitImplMetadata) { 107 | self.traits.push(impl_); 108 | } 109 | 110 | fn find_trait(&self, name: &Ident) -> Option<&TraitImplMetadata> { 111 | self.traits 112 | .iter() 113 | .find(|trait_| trait_.trait_name() == name) 114 | } 115 | } 116 | 117 | impl DiagnosticGenerator for TypeMetadata { 118 | fn modification_diagnosis( 119 | &self, 120 | other: &Self, 121 | path: &ItemPath, 122 | collector: &mut DiagnosisCollector, 123 | ) { 124 | if self.inner != other.inner { 125 | collector.add(DiagnosisItem::modification(path.clone(), None)); 126 | } 127 | 128 | // TODO: replace these O(n²) zone with a faster implentation, perhaps by 129 | // using an ordered list or a HashMap. 130 | 131 | for trait_1 in self.traits.iter() { 132 | match other.find_trait(trait_1.trait_name()) { 133 | Some(trait_2) if trait_1 == trait_2 => {} 134 | 135 | Some(_) => collector.add(DiagnosisItem::modification( 136 | path.clone(), 137 | Some(trait_1.trait_name().clone()), 138 | )), 139 | 140 | None => collector.add(DiagnosisItem::removal( 141 | path.clone(), 142 | Some(trait_1.trait_name().clone()), 143 | )), 144 | } 145 | } 146 | 147 | for trait_2 in other.traits.iter() { 148 | if self.find_trait(trait_2.trait_name()).is_none() { 149 | collector.add(DiagnosisItem::addition( 150 | path.clone(), 151 | Some(trait_2.trait_name().clone()), 152 | )); 153 | } 154 | } 155 | } 156 | } 157 | 158 | impl From for TypeMetadata { 159 | fn from(s: StructMetadata) -> TypeMetadata { 160 | TypeMetadata::new(s.into()) 161 | } 162 | } 163 | 164 | impl From for TypeMetadata { 165 | fn from(e: EnumMetadata) -> Self { 166 | TypeMetadata::new(e.into()) 167 | } 168 | } 169 | 170 | #[cfg(test)] 171 | impl Parse for TypeMetadata { 172 | fn parse(input: ParseStream) -> ParseResult { 173 | Ok(TypeMetadata::new(input.parse()?)) 174 | } 175 | } 176 | 177 | #[derive(Clone, Debug, PartialEq)] 178 | pub(crate) enum InnerTypeMetadata { 179 | Struct(StructMetadata), 180 | Enum(EnumMetadata), 181 | } 182 | 183 | impl From for InnerTypeMetadata { 184 | fn from(v: StructMetadata) -> InnerTypeMetadata { 185 | InnerTypeMetadata::Struct(v) 186 | } 187 | } 188 | 189 | impl From for InnerTypeMetadata { 190 | fn from(v: EnumMetadata) -> InnerTypeMetadata { 191 | InnerTypeMetadata::Enum(v) 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | impl Parse for InnerTypeMetadata { 197 | fn parse(input: ParseStream) -> ParseResult { 198 | input 199 | .parse::() 200 | .map(Into::into) 201 | .or_else(|mut e| { 202 | input.parse::().map(Into::into).map_err(|e_| { 203 | e.combine(e_); 204 | e 205 | }) 206 | }) 207 | } 208 | } 209 | 210 | #[derive(Clone, Debug, PartialEq)] 211 | pub(crate) struct StructMetadata { 212 | generics: Generics, 213 | fields: Fields, 214 | } 215 | 216 | impl StructMetadata { 217 | fn new(generics: Generics, fields: Fields) -> StructMetadata { 218 | let fields = fields.remove_private_fields(); 219 | StructMetadata { generics, fields } 220 | } 221 | } 222 | 223 | #[cfg(test)] 224 | impl Parse for StructMetadata { 225 | fn parse(input: ParseStream) -> ParseResult { 226 | let ItemStruct { 227 | generics, fields, .. 228 | } = input.parse()?; 229 | 230 | Ok(StructMetadata::new(generics, fields)) 231 | } 232 | } 233 | 234 | #[derive(Clone, Debug, PartialEq)] 235 | pub(crate) struct EnumMetadata { 236 | generics: Generics, 237 | variants: Vec, 238 | } 239 | 240 | impl EnumMetadata { 241 | fn new(generics: Generics, variants: Punctuated) -> EnumMetadata { 242 | let variants = variants 243 | .into_iter() 244 | .map(Variant::remove_private_fields) 245 | .collect(); 246 | 247 | EnumMetadata { generics, variants } 248 | } 249 | } 250 | 251 | #[cfg(test)] 252 | impl Parse for EnumMetadata { 253 | fn parse(input: ParseStream) -> ParseResult { 254 | let ItemEnum { 255 | generics, variants, .. 256 | } = input.parse()?; 257 | let variants = variants.into_iter().collect(); 258 | Ok(EnumMetadata { generics, variants }) 259 | } 260 | } 261 | 262 | trait ContainsPrivateFields { 263 | fn remove_private_fields(self) -> Self; 264 | } 265 | 266 | impl ContainsPrivateFields for Variant { 267 | fn remove_private_fields(self) -> Self { 268 | let Variant { 269 | attrs, 270 | ident, 271 | mut fields, 272 | discriminant, 273 | } = self; 274 | fields = fields.remove_private_fields(); 275 | 276 | Variant { 277 | attrs, 278 | ident, 279 | fields, 280 | discriminant, 281 | } 282 | } 283 | } 284 | 285 | impl ContainsPrivateFields for Fields { 286 | fn remove_private_fields(self) -> Self { 287 | match self { 288 | Fields::Named(named) => Fields::Named(named.remove_private_fields()), 289 | Fields::Unnamed(unnamed) => Fields::Unnamed(unnamed.remove_private_fields()), 290 | Fields::Unit => Fields::Unit, 291 | } 292 | } 293 | } 294 | 295 | impl ContainsPrivateFields for FieldsNamed { 296 | fn remove_private_fields(self) -> Self { 297 | let FieldsNamed { 298 | brace_token, 299 | mut named, 300 | } = self; 301 | named = named.remove_private_fields(); 302 | 303 | FieldsNamed { brace_token, named } 304 | } 305 | } 306 | 307 | impl ContainsPrivateFields for FieldsUnnamed { 308 | fn remove_private_fields(self) -> Self { 309 | let FieldsUnnamed { 310 | paren_token, 311 | mut unnamed, 312 | } = self; 313 | unnamed = unnamed.remove_private_fields(); 314 | 315 | FieldsUnnamed { 316 | paren_token, 317 | unnamed, 318 | } 319 | } 320 | } 321 | 322 | impl ContainsPrivateFields for Punctuated { 323 | fn remove_private_fields(self) -> Self { 324 | self.into_iter() 325 | .filter(|field| matches!(field.vis, Visibility::Public(_))) 326 | .collect() 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/public_api/utils.rs: -------------------------------------------------------------------------------- 1 | use syn::{AngleBracketedGenericArguments, Ident, Path, PathArguments, Type, TypePath}; 2 | 3 | pub(crate) fn extract_name_and_generic_args( 4 | ty: &Type, 5 | ) -> Option<(&Path, Option<&AngleBracketedGenericArguments>)> { 6 | let path = match ty { 7 | Type::Path(TypePath { path, .. }) => path, 8 | // TODO: handle non-path types 9 | _ => return None, 10 | }; 11 | 12 | Some((path, extract_ending_generics(path))) 13 | } 14 | 15 | fn extract_ending_generics(path: &Path) -> Option<&AngleBracketedGenericArguments> { 16 | let last_argument = path.segments.last().map(|segment| &segment.arguments)?; 17 | match last_argument { 18 | PathArguments::AngleBracketed(args) => Some(args), 19 | _ => None, 20 | } 21 | } 22 | 23 | pub(crate) fn extract_name_and_generic_args_from_path( 24 | p: &Path, 25 | ) -> Option<(&Ident, Option<&AngleBracketedGenericArguments>)> { 26 | let unique_segment = match p.segments.len() { 27 | 1 => p.segments.first().unwrap(), 28 | // TODO: handle paths with more than one segment in them 29 | _ => return None, 30 | }; 31 | 32 | let name = &unique_segment.ident; 33 | 34 | let generics = match &unique_segment.arguments { 35 | syn::PathArguments::None => None, 36 | syn::PathArguments::AngleBracketed(args) => Some(args), 37 | // TODO: handle paths with parenthesis (for instance Fn(T) -> U). 38 | syn::PathArguments::Parenthesized(_) => return None, 39 | }; 40 | 41 | Some((name, generics)) 42 | } 43 | -------------------------------------------------------------------------------- /tests/enum.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn not_reported_when_private() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | {}, 8 | { 9 | enum A {} 10 | }, 11 | }; 12 | 13 | assert!(diff.is_empty()); 14 | } 15 | 16 | #[test] 17 | fn new_enum() { 18 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 19 | {}, 20 | { 21 | pub enum A {} 22 | }, 23 | }; 24 | 25 | assert_eq!(diff.to_string(), "+ A\n"); 26 | } 27 | 28 | #[test] 29 | fn new_named_variant_field_is_modification() { 30 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 31 | { 32 | pub enum A { 33 | B {} 34 | } 35 | }, 36 | { 37 | pub enum A { 38 | B { 39 | pub c: u8, 40 | } 41 | } 42 | }, 43 | }; 44 | 45 | assert_eq!(diff.to_string(), "≠ A\n"); 46 | } 47 | 48 | #[test] 49 | fn new_unnamed_variant_field_is_modification() { 50 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 51 | { 52 | pub enum A { 53 | B() } 54 | }, 55 | { 56 | pub enum A { 57 | B(pub u8) 58 | } 59 | }, 60 | }; 61 | 62 | assert_eq!(diff.to_string(), "≠ A\n"); 63 | } 64 | 65 | #[test] 66 | fn named_field_modification() { 67 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 68 | { 69 | pub enum A { 70 | B(pub u8), 71 | } 72 | }, 73 | { 74 | pub enum A { 75 | B(pub u16), 76 | } 77 | } 78 | }; 79 | 80 | assert_eq!(diff.to_string(), "≠ A\n"); 81 | } 82 | 83 | #[test] 84 | fn empty_variant_kind_change_is_modification() { 85 | let files = [ 86 | "pub enum A { B }", 87 | "pub enum A { B() }", 88 | "pub enum A { B {} }", 89 | ]; 90 | 91 | for (id_a, file_a) in files.iter().enumerate() { 92 | for (id_b, file_b) in files.iter().enumerate() { 93 | let comparator = cargo_breaking::compare(file_a, file_b).unwrap(); 94 | let diff = comparator.run(); 95 | 96 | if id_a != id_b { 97 | assert_eq!(diff.to_string(), "≠ A\n"); 98 | } else { 99 | assert!(diff.is_empty()); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/function.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn private_is_not_reported() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | {}, 8 | { 9 | fn fact(n: u32) -> u32 {} 10 | }, 11 | }; 12 | 13 | assert!(diff.is_empty()); 14 | } 15 | 16 | #[test] 17 | fn addition() { 18 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 19 | {}, 20 | { 21 | pub fn fact(n: u32) -> u32 {} 22 | }, 23 | }; 24 | 25 | assert_eq!(diff.to_string(), "+ fact\n"); 26 | } 27 | 28 | #[test] 29 | fn new_arg() { 30 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 31 | { 32 | pub fn fact() {} 33 | }, 34 | { 35 | pub fn fact(n: u32) {} 36 | } 37 | }; 38 | 39 | assert_eq!(diff.to_string(), "≠ fact\n"); 40 | } 41 | 42 | #[test] 43 | fn generic_order() { 44 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 45 | { 46 | pub fn f() {} 47 | }, 48 | { 49 | pub fn f() {} 50 | }, 51 | }; 52 | 53 | assert_eq!(diff.to_string(), "≠ f\n"); 54 | } 55 | 56 | #[test] 57 | fn body_change_not_detected() { 58 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 59 | { 60 | pub fn fact() {} 61 | }, 62 | { 63 | pub fn fact() { todo!() } 64 | }, 65 | }; 66 | 67 | assert!(diff.is_empty()); 68 | } 69 | 70 | #[test] 71 | fn fn_arg_comma_is_removed() { 72 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 73 | { 74 | pub fn a(a: t, b: t, c: t,) {} 75 | }, 76 | { 77 | pub fn a(a: t, b: t, c: t) {} 78 | }, 79 | }; 80 | 81 | assert!(diff.is_empty()); 82 | } 83 | 84 | #[test] 85 | fn fn_arg_last_character_not_removed() { 86 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 87 | { 88 | pub fn a(a: t, b: t, c: t) {} 89 | }, 90 | { 91 | pub fn a(a: t, b: t, c: u) {} 92 | }, 93 | }; 94 | 95 | assert_eq!(diff.to_string(), "≠ a\n"); 96 | } 97 | 98 | #[test] 99 | fn empty_struct_kind_change_is_modification() { 100 | let files = ["pub struct A;", "pub struct A();", "pub struct A {}"]; 101 | 102 | for (id_a, file_a) in files.iter().enumerate() { 103 | for (id_b, file_b) in files.iter().enumerate() { 104 | let comparator = cargo_breaking::compare(file_a, file_b).unwrap(); 105 | let diff = comparator.run(); 106 | 107 | if id_a != id_b { 108 | assert_eq!(diff.to_string(), "≠ A\n"); 109 | } else { 110 | assert!(diff.is_empty()); 111 | } 112 | } 113 | } 114 | } 115 | 116 | #[test] 117 | fn is_reported_lexicographically() { 118 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 119 | {}, 120 | { 121 | pub fn a() {} 122 | pub fn z() {} 123 | } 124 | }; 125 | assert_eq!(diff.to_string(), "+ a\n+ z\n"); 126 | 127 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 128 | {}, 129 | { 130 | pub fn z() {} 131 | pub fn a() {} 132 | } 133 | }; 134 | assert_eq!(diff.to_string(), "+ a\n+ z\n"); 135 | } 136 | -------------------------------------------------------------------------------- /tests/method.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn new_public_method_is_addition() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | { 8 | pub struct A; 9 | }, 10 | { 11 | pub struct A; 12 | 13 | impl A { 14 | pub fn a() {} 15 | } 16 | }, 17 | }; 18 | 19 | assert_eq!(diff.to_string(), "+ A::a\n"); 20 | } 21 | 22 | #[test] 23 | fn new_private_method_is_not_reported() { 24 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 25 | { 26 | pub struct A; 27 | }, 28 | { 29 | pub struct A; 30 | 31 | impl A { 32 | fn a() {} 33 | } 34 | }, 35 | }; 36 | 37 | assert!(diff.is_empty()); 38 | } 39 | 40 | #[test] 41 | fn method_removal_is_removal() { 42 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 43 | { 44 | pub struct A; 45 | 46 | impl A { 47 | pub fn a() {} 48 | } 49 | }, 50 | { 51 | pub struct A; 52 | 53 | impl A {} 54 | } 55 | }; 56 | 57 | assert_eq!(diff.to_string(), "- A::a\n"); 58 | } 59 | 60 | #[test] 61 | fn signature_change_is_modification() { 62 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 63 | { 64 | pub struct A; 65 | 66 | impl A { 67 | pub fn f(i: u8) {} 68 | } 69 | }, 70 | { 71 | pub struct A; 72 | 73 | impl A { 74 | pub fn f(i: u16) {} 75 | } 76 | }, 77 | }; 78 | 79 | assert_eq!(diff.to_string(), "≠ A::f\n"); 80 | } 81 | 82 | #[test] 83 | fn generic_param_change_is_modification() { 84 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 85 | { 86 | pub struct A; 87 | impl A { 88 | pub fn f() {} 89 | } 90 | }, 91 | { 92 | pub struct A; 93 | impl A { 94 | pub fn f() {} 95 | } 96 | } 97 | }; 98 | 99 | assert_eq!(diff.to_string(), "≠ A::f\n"); 100 | } 101 | 102 | #[test] 103 | fn generic_arg_change_is_modification() { 104 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 105 | { 106 | pub struct A; 107 | 108 | impl A { 109 | pub fn f() {} 110 | } 111 | }, 112 | { 113 | pub struct A; 114 | 115 | impl A { 116 | pub fn f() {} 117 | } 118 | }, 119 | }; 120 | 121 | assert_eq!(diff.to_string(), "≠ A::f\n"); 122 | } 123 | 124 | #[test] 125 | fn not_reported_when_type_is_not_public() { 126 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 127 | { 128 | struct A; 129 | 130 | impl A {} 131 | }, 132 | { 133 | struct A; 134 | 135 | impl A { 136 | fn f() {} 137 | } 138 | }, 139 | }; 140 | 141 | assert!(diff.is_empty()); 142 | } 143 | 144 | #[test] 145 | fn is_reported_in_type_definition_path_1() { 146 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 147 | { 148 | pub mod foo { 149 | pub struct Bar; 150 | } 151 | 152 | impl foo::Bar { 153 | pub fn f() {} 154 | } 155 | }, 156 | { 157 | pub mod foo { 158 | pub struct Bar; 159 | } 160 | }, 161 | }; 162 | 163 | assert_eq!(diff.to_string(), "- foo::Bar::f\n"); 164 | } 165 | 166 | #[test] 167 | fn is_reported_in_type_definition_path_2() { 168 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 169 | { 170 | pub mod foo { 171 | pub struct Bar; 172 | } 173 | 174 | pub mod baz { 175 | impl crate::foo::Bar { 176 | pub fn f() {} 177 | } 178 | } 179 | }, 180 | { 181 | pub mod foo { 182 | pub struct Bar; 183 | } 184 | } 185 | }; 186 | 187 | assert_eq!(diff.to_string(), "- foo::Bar::f\n"); 188 | } 189 | -------------------------------------------------------------------------------- /tests/structs.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn private_is_not_reported() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | {}, 8 | { 9 | struct A; 10 | }, 11 | }; 12 | 13 | assert!(diff.is_empty()); 14 | } 15 | 16 | #[test] 17 | fn addition() { 18 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 19 | {}, 20 | { 21 | pub struct A; 22 | } 23 | }; 24 | 25 | assert_eq!(diff.to_string(), "+ A\n"); 26 | } 27 | 28 | #[test] 29 | fn removal() { 30 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 31 | { 32 | pub struct B; 33 | }, 34 | {} 35 | }; 36 | 37 | assert_eq!(diff.to_string(), "- B\n"); 38 | } 39 | 40 | #[test] 41 | fn new_public_field_tupled_is_modification() { 42 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 43 | { 44 | pub struct C; 45 | }, 46 | { 47 | pub struct C(pub u8); 48 | }, 49 | }; 50 | 51 | assert_eq!(diff.to_string(), "≠ C\n"); 52 | } 53 | 54 | #[test] 55 | fn new_private_field_tupled_is_modification() { 56 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 57 | { 58 | pub struct C(); 59 | }, 60 | { 61 | pub struct C(usize); 62 | }, 63 | }; 64 | 65 | assert!(diff.is_empty()); 66 | } 67 | 68 | #[test] 69 | fn new_public_field_named_is_modification() { 70 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 71 | { 72 | pub struct D {} 73 | }, 74 | { 75 | pub struct D { 76 | pub a: u8, 77 | } 78 | }, 79 | }; 80 | 81 | assert_eq!(diff.to_string(), "≠ D\n"); 82 | } 83 | 84 | #[test] 85 | fn new_private_field_named_is_not_reported() { 86 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 87 | { 88 | pub struct D { 89 | a: b, 90 | } 91 | }, 92 | { 93 | pub struct D { 94 | a: b, 95 | c: d, 96 | } 97 | }, 98 | }; 99 | 100 | assert!(diff.is_empty()); 101 | } 102 | 103 | #[test] 104 | fn public_named_field_modification() { 105 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 106 | { 107 | pub struct A { 108 | pub a: u8, 109 | } 110 | }, 111 | { 112 | pub struct A { 113 | pub a: u16, 114 | } 115 | }, 116 | }; 117 | 118 | assert_eq!(diff.to_string(), "≠ A\n"); 119 | } 120 | 121 | #[test] 122 | fn public_unnamed_field_modification() { 123 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 124 | { 125 | pub struct A(pub u8); 126 | }, 127 | { 128 | pub struct A(pub u16); 129 | }, 130 | }; 131 | 132 | assert_eq!(diff.to_string(), "≠ A\n"); 133 | } 134 | 135 | #[test] 136 | fn public_named_field_removal_is_modification() { 137 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 138 | { 139 | pub struct A { 140 | pub a: u8, 141 | } 142 | }, 143 | { 144 | pub struct A {} 145 | }, 146 | }; 147 | 148 | assert_eq!(diff.to_string(), "≠ A\n"); 149 | } 150 | 151 | #[test] 152 | fn public_unnamed_field_removal_is_modification() { 153 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 154 | { 155 | pub struct A(pub u8); 156 | }, 157 | { 158 | pub struct A(); 159 | }, 160 | }; 161 | 162 | assert_eq!(diff.to_string(), "≠ A\n"); 163 | } 164 | 165 | #[test] 166 | fn generic_change_is_modification() { 167 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 168 | { 169 | pub struct E; 170 | }, 171 | { 172 | pub struct E; 173 | }, 174 | }; 175 | 176 | assert_eq!(diff.to_string(), "≠ E\n"); 177 | } 178 | -------------------------------------------------------------------------------- /tests/trait_defs.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn addition_simple() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | {}, 8 | { 9 | pub trait A {} 10 | } 11 | }; 12 | 13 | assert_eq!(diff.to_string(), "+ A\n"); 14 | } 15 | 16 | #[test] 17 | fn trait_item_addition() { 18 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 19 | { 20 | pub trait A {} 21 | }, 22 | { 23 | pub trait A { type B; } 24 | }, 25 | }; 26 | 27 | assert_eq!(diff.to_string(), "+ A::B\n"); 28 | } 29 | 30 | #[test] 31 | fn trait_item_modification() { 32 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 33 | { 34 | pub trait A { 35 | type B = u8; 36 | } 37 | }, 38 | { 39 | pub trait A { 40 | type B = u16; 41 | } 42 | }, 43 | }; 44 | 45 | assert_eq!(diff.to_string(), "≠ A::B\n"); 46 | } 47 | 48 | #[test] 49 | fn trait_item_removal() { 50 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 51 | { 52 | pub trait A { 53 | type B; 54 | } 55 | }, 56 | { 57 | pub trait A {} 58 | }, 59 | }; 60 | 61 | assert_eq!(diff.to_string(), "- A::B\n"); 62 | } 63 | 64 | #[test] 65 | fn trait_item_kind_modification() { 66 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 67 | { 68 | pub trait A { 69 | type B; 70 | } 71 | }, 72 | { 73 | pub trait A { 74 | const B: usize; 75 | } 76 | }, 77 | }; 78 | 79 | assert_eq!(diff.to_string(), "- A::B\n+ A::B\n"); 80 | } 81 | 82 | #[test] 83 | fn in_private_module() { 84 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 85 | {}, 86 | { 87 | mod a { 88 | pub trait A {} 89 | } 90 | }, 91 | }; 92 | 93 | assert!(diff.is_empty()); 94 | } 95 | 96 | #[test] 97 | fn in_public_module() { 98 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 99 | { 100 | pub mod a {} 101 | }, 102 | { 103 | pub mod a { 104 | pub trait A {} 105 | } 106 | }, 107 | }; 108 | 109 | assert_eq!(diff.to_string(), "+ a::A\n"); 110 | } 111 | -------------------------------------------------------------------------------- /tests/trait_impls.rs: -------------------------------------------------------------------------------- 1 | use cargo_breaking::ApiCompatibilityDiagnostics; 2 | use syn::parse_quote; 3 | 4 | #[test] 5 | fn addition_simple() { 6 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 7 | { 8 | pub struct T; 9 | }, 10 | { 11 | pub struct T; 12 | 13 | impl A for T {} 14 | }, 15 | }; 16 | 17 | assert_eq!(diff.to_string(), "+ T: A\n"); 18 | } 19 | 20 | #[test] 21 | fn modification_simple() { 22 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 23 | { 24 | pub struct T; 25 | 26 | impl A for T {} 27 | }, 28 | { 29 | pub struct T; 30 | 31 | impl A for T {} 32 | } 33 | }; 34 | 35 | assert_eq!(diff.to_string(), "≠ T: A\n"); 36 | } 37 | 38 | #[test] 39 | fn provided_method_implementation_is_not_reported() { 40 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 41 | { 42 | pub struct S; 43 | 44 | impl T for S {} 45 | }, 46 | { 47 | pub struct S; 48 | 49 | impl T for S { 50 | fn f() {} 51 | } 52 | }, 53 | }; 54 | 55 | assert!(diff.is_empty()); 56 | } 57 | 58 | #[test] 59 | fn constant_modification_is_modification() { 60 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 61 | { 62 | pub struct S; 63 | 64 | impl T for S { 65 | const C: usize = 0; 66 | } 67 | }, 68 | { 69 | pub struct S; 70 | 71 | impl T for S { 72 | const C: usize = 255; 73 | } 74 | }, 75 | }; 76 | 77 | assert_eq!(diff.to_string(), "≠ S: T\n"); 78 | } 79 | 80 | #[test] 81 | fn type_modification_is_modification() { 82 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 83 | { 84 | pub struct S; 85 | 86 | impl T for S { 87 | type T = u8; 88 | } 89 | }, 90 | { 91 | pub struct S; 92 | 93 | impl T for S { 94 | type T = u16; 95 | } 96 | }, 97 | }; 98 | 99 | assert_eq!(diff.to_string(), "≠ S: T\n"); 100 | } 101 | 102 | #[test] 103 | fn impl_trait_order_is_not_tracked() { 104 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 105 | { 106 | pub struct S; 107 | 108 | impl T1 for S {} 109 | impl T2 for S {} 110 | }, 111 | { 112 | pub struct S; 113 | 114 | impl T2 for S {} 115 | impl T1 for S {} 116 | }, 117 | }; 118 | 119 | assert!(diff.is_empty()); 120 | } 121 | 122 | #[test] 123 | fn not_reported_when_type_is_not_public() { 124 | let diff: ApiCompatibilityDiagnostics = parse_quote! { 125 | { 126 | struct S; 127 | 128 | impl T for S {} 129 | }, 130 | { 131 | struct S; 132 | } 133 | }; 134 | 135 | assert!(diff.is_empty()); 136 | } 137 | --------------------------------------------------------------------------------