Last year's annual review post observed that cargo-semver-checks' lint library is undergoing exponential growth, doubling each year: 30 lints at the end of 2022, 57 lints in 2023, and 120 at the end of 2024. We bring 2025 to a close with 242 lints, more than doubling last year's total — and that's just one facet of what we accomplished. Let's look at the full picture, and the path for 2026 and beyond!
The exponential curve fits like a glove! For bonus points, see if you can tell when major holidays allow for major bursts of activity in the project 😁
At a high level, cargo-semver-checks is growing faster than ever. By the numbers:
- 7 new
cargo-semver-checksreleases, from v0.39 to v0.45 - 122 new lints, more than doubling last year's count
- 734 commits from 18 authors
The code
cargo-semver-checksdepends on is split between 3 different repositories, for technical reasons outside the scope of this post. The statistics include all those repositories. The author count includes two bot accounts that we use for automated maintenance tasks: Dependabot, and an automated workflow that adds new Rust versions to our test matrix. - 4x reduction in total lint execution time — some lints became up to 10x faster The 4x number here was measured over the fixed set of lints available at the time. Of course, we then added even more lints, which makes simple wall-clock time comparisons a case of comparing apples and oranges.
- 26 (!!) different format versions of rustdoc JSON
cargo-semver-checksv0.39 from January 2025 supported rustdoc JSON formats as old as v32. We ended 2025 on rustdoc JSON format v57. were released throughout the year, without users ofcargo-semver-checksneeding to know or care — everything just worked. This is one of the key ideas that makescargo-semver-checksviable at all. A high-level overview is available here, and a more detailed explanation is part of my RustConf 2024 talk.
All this was made possible through the funding provided by the Rust Foundation Fellowship Program, Amazon, Rerun, Accelerant, Astral, Zoo, the generous individuals who support my work via GitHub Sponsors, as well as the Google Summer of Code program where I was a mentor. To everyone who makes what I do possible, thank you 🙏
On behalf of the cargo-semver-checks project, I'm also proud to recognize jyn's irreplaceable and tireless work in the Rust community with a $1000 award via GitHub Sponsors. Thank you, jyn! More info about this is available here.
Here's this year's recap, and the plan going forward:
- Goal: Fearless
cargo update - What we shipped in 2025
- Conference talks and podcast appearances
- The path forward for 2026 and beyond
Goal: Fearless cargo update
Skip ahead if you are already familiar with cargo-semver-checks.
Our goal is simple: cargo update should be fearless.
In an ideal world, all libraries would perfectly adhere to SemVer — but we've seen over and over that SemVer is near impossible to adhere to without automated help.
cargo-semver-checks is that automated help: a linter that flags SemVer violations before publishing new releases that break cargo update downstream.
The rest of this post describes our progress toward this goal in 2025, and our plans for 2026 and beyond.
What we shipped in 2025
While reading through this section, you may notice a few themes:
- Shipping 122 new lints was a colossal endeavor.
- Even ignoring the tool itself, Rust directly benefited from the research that
cargo-semver-checksrequired. - A lot of "behind the scenes" work is what makes
cargo-semver-checkspossible.
122 new lints is a lot
122 lints in 365 days means writing a new lint every 3 days.
It's actually much more than just writing and code-reviewing the lint itself — it also requires all the background work to make that lint feasible to write in the first place:
- analyzing all edge cases of the SemVer rule being enforced
- extracting the data from rustc into rustdoc JSON
- making it queryable with the Trustfall query engine
- backporting that code to support older rustdoc JSON versions
- writing test crates to ensure the lint triggers when it should — and does not trigger when it shouldn't
The new lints cover a wide area of the Rust language:
- 17 lints for breakage in generic parameters
- 14 lints covering items becoming
#[deprecated] - 14 lints around method receivers, like changing
&selfinto&mut self - 12 lints for breakage of hardware requirements in
#[target_feature] - 10 lints for breakage in functions' ABI and/or exported symbols
- 10 lints for changes in types'
reprand/or sort ordering semantics - 8 lints for changes related to sealed traits
- 7 lints for breakage in type kinds, like changing
structtoenum - 6 lints for breakage in function return values
- 5 lints for changes to
const,unsafe,Copyimpls, and traitdyncompatibility - 2 lints for changes in crate features specified in
Cargo.toml - 17 other informative, opt-in lints for additive (non-breaking) changes A commonly-requested feature is the ability to report additive-only changes, for example for use in crates that rely heavily on code generation. Those lints are disabled by default and purely opt-in. We're steadily building up our coverage here — this is an easy area where new contributors can make an impact!
The pace was relentless, and at times, a struggle to keep up with.
I'm simultaneously proud of achieving this feat, and also deeply aware that we've found the limit of what a solo maintainer of an OSS project could do without burning out.
Despite the generosity of a handful of companies and individuals, cargo-semver-checks isn't even remotely close to being well-funded enough to be my full-time job.
As a result, two things are simultaneously true:
- I get a massive amount of motivation from seeing folks become GitHub Sponsors of
cargo-semver-checks— the implied moral support is just as valuable as the monetary value of the sponsorship itself. - And yet, there are only 24h in a day and 7 days in each week, many of which are taken up by my full-time job that isn't related to
cargo-semver-checks. In practice, this means I do well over 40h of coding work each week — a punishing tempo to maintain year after year.
This is likely the end of the exponential lint growth of cargo-semver-checks.
Barring a surprise set of large new corporate sponsorships for the project,
The astute reader likely noticed that the list of corporate sponsors of cargo-semver-checks remained unchanged between the previous year in review post and this one, despite the massive growth of both the project itself and its adoption in the Rust community. This is disappointing, but cargo-semver-checks is hardly unique in this aspect. It unfortunately seems to be how 2025 shaped up in the OSS community in general. If your company relies on open-source software, please look into ways to fund those projects!
I just don't see a way to reasonably write or even just code-review 240 new lints in 2026.
But I think that's okay, and even actively desirable in some ways!
I wrote more about the path forward for 2026 and beyond later in this post.
Discovering a soundness bug in Rust
This recaps the work described in this post — if you've already read it, feel free to skip ahead.
My process for researching ways to cause breakage with a Rust language feature involves writing lots of cursed Rust programs aimed at probing the limits of what Rust allows. It's crucially important to know precisely what is and is not allowed — often to a much deeper level of detail than what the Rust book, the language reference, the Rustonomicon, and relevant RFCs specify.
While researching ways to cause breakage with the #[target_feature] attribute, I wrote many such cursed Rust programs.
To test my understanding of #[target_feature], I wrote a program I knew was problematic.
To my surprise, rustc accepted it without errors!
This was a tricky soundness bug in the design of the #[target_feature] attribute. Allowing a particular desirable but rare use case resulted in the possibility of undefined behavior in purely safe Rust — the Rust reference terms such code unsound.
The resulting collaboration with other Rustaceans over the next three months made Rust better for everyone:
- A new RFC evolved
#[target_feature]to close the soundness hole while also offering better support for those tricky use cases. - Several
rustdocbugs were discovered and fixed. cargo-semver-checksgained a dozen new lints for all sorts of tricky breakage.
A win-win for everyone — including Rustaceans who have never even heard of cargo-semver-checks!
Doubling the lints without doubling the runtime
At a high level, if running N lints in a project takes S seconds, we expect running 2 * N lints would take 2 * S seconds.
An exponentially growing number of lints would then mean an exponentially longer time to run all of them. This is awful for the user experience!
Less obviously, it's also atrocious for the developer and contributor experience too — in fact, it's even worse! Here's how:
- The
cargo-semver-checkstest suite consists of a set of test crate pairs representing the "old" and "new" versions of a project being checked. - Broadly speaking, adding a new lint results in one or more such test crate pairs being added too.
- The tests run all lints on all "old vs new" crate pairs to ensure lints flag everything as expected. But they also run all lints on unchanged source code (so "old vs old" and "new vs new") asserting that no lints trigger for any test crate — thus preventing one type of false positives. This check has prevented around a dozen subtle bugs so far — it's certainly been much more valuable than I may have expected ahead of time!
- Testing
Nlints onNtest crate pairs results in3 * N * Ntotal lint executions.2 * Nlints means2 * Ntest crate pairs, quadrupling the total lint executions in the test suite to3 * (2 * N) * (2 * N).
This hurts. Midway through 2025, our test suite was running more than 250,000 lint executions and taking 7min+ to run — or even more if rustdoc JSON needed to be rebuilt first.
As part of a Google Summer of Code project, contributor Joseph Chung worked on optimizing both lint execution and our test harness. The final results were quite impressive!
- Large crates saw 4x faster lint execution times.
- Instead of 7min, the test suite would complete in just 1 minute!
Joseph's blog post has the full details of what it took to accomplish this!
Why compiling the same Rust program might succeed or fail depending on the current directory
This recaps the work described in this post — if you've already read it, feel free to skip ahead.
A user reported that cargo-semver-checks claimed their crate had compile errors, but a regular cargo check on the same crate found no errors. A very strange case!
Debugging this problem required going down the rabbit hole of:
- the contents of
.cargo/config.tomlfiles - how
cargochooses the config file to apply, if there are multiple options - ways that
cargo checkcan pass butcargo doccan fail, or vice versa - the number of different ways to set
RUSTFLAGSandRUSTDOCFLAGS
The bug turned out to be an inconsistency between the user's desired RUSTFLAGS or RUSTDOCFLAGS values and the ones that cargo-semver-checks would use, due to a difference in which .cargo/config.toml file is loaded based on the current working directory.
To fix the bug, we made cargo-semver-checks replicate cargo's algorithm for finding and choosing .cargo/config.toml files and applying the configuration they specify, including in situations where cargo-semver-checks is running on one target triple but SemVer-linting for another.
In service of doing all this correctly, we added 28 new test cases checking many cursed crate and configuration combinations.
Find the full details in this blog post!
Resolving the edge cases of #[doc(hidden)], SemVer, and sealed traits
This recaps the work described in this post — if you've already read it, feel free to skip ahead.
Breaking the way traits get implemented is okay, from a SemVer perspective, if no such downstream implementations could have existed in the first place. Such traits are called "sealed." If nobody's code could be broken, the breakage may as well have never happened.
But here's some nuance: one can have a trait that is technically able to be implemented downstream, but where the implementation is forced to rely on non-public API — for example, items marked #[doc(hidden)].
This is useful if a "core" crate needs to define a trait, and a "macros" crate from the same overall project needs to generate implementations for that trait.
Those macro-generated implementations live in the downstream user's crate, meaning the trait cannot be fully sealed.
And yet the "core" crate wants to signal that only the "macros" crate is intended to offer such trait implementations, so the trait is sealed from a "public API" perspective: its public API does not allow implementations.
This year, cargo-semver-checks gained the ability to detect when traits are sealed in their public API and accurately classify impl-breaking changes accordingly.
It also began correctly handling yet another newly-discovered way to seal traits: via associated const items!
Instead of a performance regression, this new functionality came with optimizations that made sealed trait analysis run faster than ever! Find the full details in this post.
Conference talks and podcast appearances
I gave three conference talks and was a guest on two podcasts this year.
What it'll take to eradicate unintended breakage from Rust — RustWeek
In May, I had the honor of being invited to give a talk in the Rust Project track of RustWeek in the Netherlands. My talk was titled "What it'll take to eradicate unintended breakage from Rust" and had the following pitch:
Unintentional breakage sucks for everyone involved. It’s a sociotechnical problem, and we need to do better on both the “socio” and the “technical” parts. SemVer is a part of it, but just a small part: it doesn’t cover all aspects of stability, some stability guarantees are fuzzy, some changes are “breaking but not major” etc. If running
cargo updatebreaks my program, being told “sorry, that change was breaking but not major” isn’t any consolation!Solving this will require a user-centric view of breakage. Acting upon that view will require the enthusiastic participation of a broad set of folks who will be in the room. At minimum: rustdoc, cargo, lang, compiler, libs, crates.io + docs.rs. That’s a lot of people! But here we get to decide what the experience of using Rust in 2030 and beyond is like, and I’d like to make a pitch for why this investment will pay off spectacularly if we do it right.
The Past, Present, and Future of SemVer in Rust — Rust Forge
In August, I was delighted to visit Aotearoa New Zealand for the first time in order to attend Rust Forge. Rust Forge had phenomenally delicious food at the event, and that taught me a lesson about inclusivity that I'll never forget. Initially, I was surprised I didn't see signage pointing out where to find the vegan and vegetarian options ... then it hit me: all the food was vegan. Everyone raved about how good the food was, and everyone was maximally included in enjoying all of it. Nicely done! My hat is off to the organizers 🙇♂️
My talk was titled "The Past, Present, and Future of SemVer in Rust" and had the following pitch:
At least 1-2 times per week, accidental breaking changes sneak into the new release of some popular Rust package, despite the maintainers' best efforts. Much frustration usually follows!
How did we get here? Why is this still a problem after 10 years of stable Rust? Hasn't
cargo-semver-checkssolved this already? If not, what will it take?Fearless
cargo updateis our goal — let's talk about how to make that happen!
Cursed Rust — A Surprise Talk at Rust Forge
By popular request from the Rust Forge audience, I had a surprise re-appearance later that same day for an impromptu talk on cursed Rust!
The goal was to squeeze into 20ish minutes as many examples of why checking SemVer by hand in Rust is horrendously difficult. Featured in the talk:
- why adding a private field to a
pub structis only sometimes breaking - how SemVer is a function of community norms as much as a collection of rules
- how some of today's breaking changes may stop being breaking in the future
- how a Rust crate can have an infinitely large public API, and still compile
- a ton of examples of seemingly "doing everything right" and still causing breakage!
I'll be honest, I was like a kid in a candy store with this talk: I had so much fun. I hope you enjoy it too!
A Universal Query Engine in Rust — Developer Voices
In February, I had the pleasure of joining Kris Jenkins on Developer Voices to talk about the Trustfall query engine and how it makes cargo-semver-checks possible.
cargo-semver-checks — Open Source Security
Then in April, it was my pleasure to join Josh Bressers on Open Source Security and chat about how making version upgrades smoother makes software both safer and better for everyone.
The path forward for 2026 and beyond
Exponential growth is what got cargo-semver-checks to the present, and it was wonderful.
Is continued exponential growth something we should aim for as a goal in 2026 and beyond?
I'm reasonably convinced the answer is "no" — and I have a concrete alternative to propose. Hear me out, and then I'd like to hear your thoughts via your preferred social media channel.
The best metric for cargo-semver-checks is "how much real-world breakage is it catching and preventing."
That's why the project exists at all, right?
Of course, that metric is difficult to measure directly. Our 2023 study that SemVer-linted 14000+ releases of the top 1000 most popular crates took approximately 4 person-years of effort to complete!
For many years, the number of lints was a decent proxy for that metric. We had relatively few lints and lots of low-hanging fruit, so it wasn't unreasonable to assume that going from 1 lint to 10, to 100 was approximately tracking the amount of breakage we were likely to catch.
With 242 lints (and still growing!), the number of lints by itself is on the cusp of becoming a vanity metric: one that makes us feel good, but is no longer a reliable measurement of our underlying goals. We have never attempted to "game" this number, and I'd prefer to keep it that way. This is not to say we won't keep adding lints — we absolutely will keep adding them by the dozens as I explain below!
Instead, here's what I propose for the future:
- By the end of 2027, we should aim to have the ability to do ecosystem-level SemVer linting runs within person-weeks of effort, not person-years — a ~50x improvement. We should complete at least one such run, and publish an update to our 2023 SemVer study with a mind to presenting its results to the Rust community at a suitable event such as RustWeek or RustConf 2028. The 2023 study looked at a five-year period of releases, so having a five-year step forward between the original study and the update feels right to me somehow. Let me know what you think!
- Before then, we should invest significant effort in resolving the two hardest open problems in
cargo-semver-checks:- Performing type-checking in lints, so we can catch changes like
pub fn example(x: i64) {}becomingpub fn example(x: String) {}. This is our most commonly requested feature today, and will resolve the largest remaining class of false-negative outcomes! A false negative is a case where breakage happened, but we did not raise a lint. - Linting across crate boundaries with high reliability and acceptable performance, so that for example, we can correctly handle cross-crate item re-exports. This will resolve the largest remaining class of false-positive bugs in our linting system. A false positive is a case where a lint fired inappropriately, and the claimed breakage does not exist. We consider such cases to be bugs.
- Performing type-checking in lints, so we can catch changes like
- By completing these milestones, we'll have resolved the largest remaining blockers for merging
cargo-semver-checksintocargo! We should then make a case that the work of performing that merge should begin in earnest!
The challenge in these work streams is that they require a significant amount of effort to even get started toward working on them. They require a large amount of context that is difficult to retain without significant and regular contributions, such as the Google Summer of Code (GSoC) model where contributors spend dozens of hours per week over the span of 10-20 weeks on their project. I propose that we direct my energy, as well as the energy of future GSoC contributors and any other regular contributors wishing to take on that level of responsibility, toward tackling those goals.
We should still expect our lint count to grow significantly while pursuing these goals. Writing lints remains an excellent area for new contributors to the project, as well as for contributors that do not have the ability to commit dozens of hours per week for an extended period of time. We are deeply grateful for all such contributions, and we hope they continue!
Additionally, as we move toward type-checking lints, we expect to open up new areas of Rust to SemVer linting — making hundreds more lints available to write.
These lints will offer unique challenges, but also make cargo-semver-checks massively more capable and able to detect breakage.
We should expect the proposed SemVer survey to show that those new lint categories are extremely impactful, and conclusively demonstrate that the effort was worth it.
We've already started down this path
We've taken at least two strides down this path already.
As part of Google Summer of Code 2025, Talyn worked on adding witness generation to cargo-semver-checks.
Her post is excellent and has all the details, which I'll briefly summarize.
The ultimate proof that SemVer breakage happened is showing a concrete program that actually suffers the claimed breakage. We call this a "witness" program. Witnesses are necessary to analyze SemVer breakage at scale in the Rust ecosystem, and also a necessary part of type-checking lints: we rely on the Rust compiler to tell us whether a change in types is breaking or not. This lets us handle the full complexity of Rust's type system, without having to reimplement a borrow checker, type checker, trait solver, etc.
We need cargo-semver-checks to both generate and check the validity (via cargo check) of such witnesses, for all current lints and all future ones.
Talyn's work bootstrapped a system that allows us to do so, and I'm very excited to make more use of it going forward!
The second step down this path is the progress jyn and aDotInTheVoid have been making toward making it possible to connect rustdoc JSON files.
For example, rustdoc JSON now includes the rlib path of other crates whose items were referenced.
For the first time in years, we have a concrete path forward and meaningful momentum to get there!
The key moment happened at RustWeek 2025, when jyn guided ~15 stakeholders through 2h of intense technical discussion until we emerged with a workable design for cross-crate rustdoc JSON.
For this and other outstanding contributions to Rust, on behalf of the cargo-semver-checks project it was my pleasure to recognize jyn's ongoing positive influence with a $1000 award via GitHub Sponsors.
Expect more on both of these work streams in future posts!
What success will require, and how you can help
In short, funding. Fund your dependencies, so you encourage the work that keeps them going!
Not just cargo-semver-checks, but all of Rust and all the open-source software your work relies on.
Everyone who works on Rust and open-source tooling could make way more money investing their precious expertise elsewhere. Things we don't value are things we stand to lose.
The Rust community has already lost many contributors this year due to lack of funding. Many other folks, myself included, work on open source only on personal time — nights and weekends.
cargo-semver-checks is not a for-profit enterprise.
But if it were, how high up on the OSS funding leaderboard would its $1000 donation place it?
Where does your employer rank on that leaderboard? And — why?
Organizations like Open Source Pledge can help you get started.
If you liked this essay, consider subscribing to my blog or following me on Mastodon or Bluesky. You can also fund my writing and work on cargo-semver-checks via GitHub Sponsors, for which I'd be most grateful ❤