[RFC] Add non-blocking precommit `clang-tidy` analysis

TL;DR

Add non-blocking precommit clang-tidy analysis to LLVM’s CI on per-component basis. Component maintainers (like mlir, clang, lldb, ..) will decide if precommit clang-tidy analysis is needed. This RFC don’t propose to enforce clang-tidy on whole LLVM at once. If you feel that enabling clang-tidy for your component will be a overall a net-loss, feel free not to do so.

Rationale

clang-tidy is a C++ linter tool developed under LLVM umbrella.
Having it in precommit CI will give us the following benefits:

  1. Less load on maintainers during review: clang-tidy can automatically check naming style, LLVM code conventions. Most importantly, contributors will be able to fix those issues in advance, before intaraction with human reviewer.
  2. Less chance of missing subtle errors that could be hard to spot in big patches.
  3. Create a quality gate for the project and give opportunity to modernize codebase piece by piece (by using modernize-* checkers).

Implementation plan and Coordination with maintainers

Currently, we have a working prototype of premerge CI that was tested against clang-tools-extra/clang-tidy directory for approx. half a year. Here is example of how clang-tidy works when e.g. incorrect variable style is used.

To kickstart wide adoption of clang-tidy, we need maintainers who are willing to help with initial setup and feedback.
Target adoption plan:

  1. Choose initial set of components to enable clang-tidy.
  2. Choose checks to enable in those components (they are most likely already present in .clang-tidy configs of those components).
  3. (Optional) Clear relevant part of codebase from existing warnings of those checks.
  4. Setup CI infrastructure to run clang-tidy in premerge. A new config will be introduced to specify which listed directories will have clang-tidy enforced in precommit, e.g:
    clang-tools-extra/clang-tidy/.*
    mlir/.*
    
    This config means that only files under “mlir” or “clang-tidy” will be analyzed in CI (even if patch touches other files outside those directories).
  5. Write guidelines on how to add new LLVM components to clang-tidy so process can be automated and no further technical involvement from Infrastructure team would be needed.
  6. More LLVM components gradually enable clang-tidy when have resources for it.
5 Likes

CC who may interested in this proposal
@mehdi_amini, @kuhar, @shafik, @jurahul

Given my current proposal recommends that we enable clang-tidy for Rule of Three checking, I think this is a good idea. At minimum I think at least setting up the framework so that sub-projects can then make a choice on whether this makes sense for them is a good first step.

It is not clear to me how much of a resource burden this will be for pre-commit testing, I hope minimal. It also seems like we have a useful tool for catching common bugs we should be using it, if we have the resources.

It should be minimal. The only concern we need to worry about is maximum concurrency around the free Github Actions Runners (only 60 concurrent jobs at a time). We have hit that before, but it’s pretty rare. I don’t think this will add significant load. These jobs should all finish in <10 minutes, and for premerge we don’t usually see more than 16 concurrent jobs for jobs that take ~30 minutes. If we run into capacity constraints here, we could probably look at utilizing some of the premerge resources to run these jobs given we should have sufficient extra capacity there.

Github was supposed to give as an enterprise account at some point too, which would give us 1000 concurrent jobs on the free Github Actions Runners, but I’m not sure of the current status of that. That would make any resourcing concerns a non-issue.

4 Likes

Revising my earlier opinion on the topic after clarifying that clang-tidy is built into Visual Studio (and I imagine there are plug-ins for other common IDEs), soft +1. I do have some questions/comments:

  1. It should be clear in the documentation for the relevant components how to run clang-tidy on just the things changed in a given diff, like it is for clang-format, so that developers can reproduce and fix issues without getting complaints about unrelated code.
  2. Will the process work the same as the clang-format checker? In other words, will a comment be posted describing the failure(s) and if so, will that comment be updated once the issue has been fixed?
  3. As I understand it, this isn’t proposing to enable clang-tidy for core LLVM yet. That being said, are all the policies enforced by clang-tidy documented in the regular coding standards? It would be unfortunate for pre-commit to complain about a given piece of code that appears to be fully compliant with the documentation. I certainly don’t think it should be as simple as saying in the documentation that you should just follow what clang-tidy says, because then it’s not clear what the path forward is when writing code, if you don’t have easy access to clang-tidy for whatever reason or simply don’t want to spend the time running it.

clang-tidy is backed into clangd Language server, so If one uses clangd to navigate C++ code in any IDE (vscode, nvim, clion,..) then warning from clang-tidy are automatically available with --clang-tidy flag (just like warning from compiler flags).

In prototype, It is already working like that. You can look at “You can test this locally with the following command” in demostration comment.

Yes, it is not working same as clang-format (using same workflow to post/update comment under the hood)

clang-tidy by default doesn’t enforce anything (in other words doesn’t have any checks enabled by default). So any check that clang-tidy will run/enforce must be hand-picked into .clang-tidy config by someone (most likely maintainers of a component). That said, I deliberately try to avoid specific detail about checks that it will enforce because it should be different proposals like this or this.
But generally speaking, when creating .clang-tidy config, maintainers can go conservatively and enable only llvm-* checks which enforce only LLVM coding conventions documented here.

clang-tidy is really nice and I use it heavily in MLIR, however it also yields a lot of false positives (it suggests incorrect edits, that may not even compile), which to me is hard to enforce as “on-by-default precommit warning system”. The specific checks should be very curated before exposed to a user: otherwise we’re asking the author to spend previous human time on disproving false positives.

Have you considered enabling this on an opt-in basis? I could imagine the same mechanism as the Copilot reviewer: you can add a specific “clang-tidy checker” account as reviewer of a PR and the automation would only then run and publish the analysis.

Another thing could also be to try to apply the edits proposed by clang-tidy, try to rebuild the project with the edits, and only if these edits are passing the tests then they could be shown to the users.

I reported last summer some examples of invalid diffs proposed by clang-tidy (the edits would just not build):

I’m in support of the general idea, though I would caution that clang-tidy is where opinionated checks live and so there can be a higher false positive rate than some may want for certain checks. But given that each component can determine which checks make sense for the component and only run the ones they really want to, I think that’s fine (and it hopefully encourages us to find ways to reduce the false positives or reduce the friction with handling them).

That said, I would like us to not use lint comments to locally suppress false positives. These kinds of comment markup do not scale well, get out of sync with reality too easily, and I think we should avoid them in general (same goes for IWYU, clang-format, and other comment markups).

1 Like

I second this. If clang-tidy was just opinionated, it would be fine. But when it gets things wrong, it makes it harder for new developers to converge on a good PR as they struggle to debug crashes after applying the proposals.

I’m supportive of the general idea of running clang-tidy by default. We have it enabled in the IREE CI and found it useful, but it does have a fair bit of false positive, e.g., suggesting to make public API internal, or complaining about chained comparisons in templated code (confusion around angle brackets). I’d suggest to enable it but as a non-blocking check.

Based on the experience running it in IREE, I think it would also be worth considering building it from source (ever so often), instead of relying on a fixed release, or at least always using the most recent release. This incentivises fixing false positives and adding new llvm-specific checks. We do this in IREE and it wasn’t difficult to set up iree/.github/workflows/build_clang_tidy.yml at main · iree-org/iree · GitHub .

1 Like

+1 to this sentiment, I think we want to aim for checks that are finding definite bugs and not stylist checks. That is why I suggested for the Rule of Three, it is definitely a bug to use default copy/assign to a class that has ownership semantics, with a few exceptions and even in these cases it can surely be argued that being explicit is the better policy.

I have not done any sort of audit, so I don’t know how many checks really hit this bar, maybe it is high bar but leaving it to each sub project should be sufficient. We may realize that some checks don’t meet the bar after a few PRs, we can easily disable those checks. It will likely be good for the clang-tidy folks to see where checks need more work.

I will slightly disagree that we have a lot of false positives in clang-tidy: as of today, there is almost 400 checks and 117 false-positives issues. Assuming all issues are covering different checks, still ~70% of all check don’t have false positives (and should be probably fine to use).
I agree that only checks that at least do not have any false positives on current codebase should be enforced in CI.

Since we have a lot of checks, explicitly requesting review from each one by hand seems very time consuming. I can imagine a button to request review from all configured checks at once, but it still have to be done on each PR, right? Otherwise we will have different quality gates for different PRs in one component: half the reviewer request checks, half not. It can bring furher inconsistentce to codebase (and probably cause dissatisfaction of people that actually fix clang-tidy warnings).

Generally, not opposed to workflow with explicit request from “clang-tidy reviewer” If this is the only thing that stops folks from using the tool. However, I don’t think it scales well on a diverse codebase like LLVM with hundreds of individual reviewers.

I think this is a valid workflow to modernize codebase locally, but not in CI.
Folks would still run clang-tidy locally first (likely before putting a PR) and will waste time on dealing with errors only to find out that wasn’t even needed to begin with. If some checks are having a significant number of incorrect fixes, I’d suggest to disable them entirely from CI.

Also, rebuilding code as a part of linting workflow will likely take a considerable amont of resouces and may not be possible on github provided runners.

1 Like

Thanks for redirecting, this is totally fair and accurate: my concern was only about specific checks indeed!
“clang-tidy” isn’t a monolithic tool so it is hard to talk about it as such…

Yes, unfortunately the quality of checks can be quite different. And sometimes checks aim at unreachable goals, thus have highier number of false positives (take e.g. const-correctness).

1 Like

IMHO, we should also take into account contributors/reviewers ratio. If reviewers bandwidth is very limited, it is probably fine that 1/100 PRs may have problems with false positives, but other 99 saved hours of manual review.


Initially, clang-tidy was introduced to lower burden of review in clang-tidy codebase because
entry threshold is quite low in clang-tidy and a lot of newcomers made same simple mistakes like not following LLVM naming conventions (most likely because they are used to “main job codestyle”)

Please note that some checks implement coding guidelines different from LLVM’s. Such checks are disabled even for Clang-Tidy code.

Any chance of a better integrated experience? Carbon, for instance, uses something called reviewdog to integrate clang-tidy checks that produce automated comments with fixit suggestions, eg: Implement correctly overriding dependent virtual functions by dwblaikie · Pull Request #5804 · carbon-language/carbon-lang · GitHub

Thanks for pushing this forward — I like the direction. I do think we can make the proposal both more precise and more effective by tightening the “what blocks” semantics, and by making the results show up where review actually happens.

The first thing I’d change is the way we decide whether the linter “failed”. Right now the helper seems to infer failure from “did the tool print anything to stdout/stderr”, which is the wrong contract for clang-tidy (and for the whole idea of mixing blocking vs informational diagnostics). See code-lint-helper.py around here: llvm-project/llvm/utils/git/code-lint-helper.py at main · llvm/llvm-project · GitHub. Output can be perfectly valid in a non-blocking mode (warnings, notes, noisy checks we’re still evaluating), so using “any output == fail” makes the rollout policy ambiguous and brittle.

A clean way to fix this is to make the blocking policy come from clang-tidy’s native configuration, and have CI only look at the exit code. Clang-tidy already supports granular “these warnings are errors” via WarningsAsErrors in .clang-tidy. That’s also the intuitive single source of truth for both CI and local runs: the wrapper/script doesn’t need to encode policy, it just runs clang-tidy and blocks only if the configured “errors” are present (non-zero exit).

On the “non-blocking vs blocking” question: I don’t think it should be one mode forever. Non-blocking is great as an adoption ramp, but long-term it’s valuable to explicitly split checks into two buckets: (1) checks we want to enforce because they’re low false-positive / largely mechanical, and (2) checks that are useful but more heuristic/noisy, which can remain informational until proven stable. This is also where the stylistic-check objection can be addressed constructively: rather than “no style checks”, we can say “style checks are fine, but they graduate to blocking only when they are useful and demonstrate low false-positive rates”. For LLVM in particular, the deterministic style/convention checks are exactly the ones that save reviewer bandwidth (and avoid humans repeating the same comments), but we should earn the right to block merges with them.

To make “graduation” concrete, we could use a simple measurable rule: start a check as non-blocking, and promote it to blocking once its observed false-positive rate drops below N% (with N agreed by the community and allowed to differ by check/component). In practice, N should likely be quite low for checks that are expected to be deterministic, and only higher for inherently heuristic checks if a component explicitly opts in. False positives can be handled with targeted suppressions (e.g. // NOLINTNEXTLINE(<check-name>)), but the main failure mode to watch for is “just-in-case” or copy-pasted suppressions that end up masking real issues. To keep that under control, it helps to make suppressions auditable via clang-tidy itself — either through configuration options or a dedicated check: forbid non-targeted NOLINTs, and treat it as an error when a targeted suppression doesn’t actually suppress any diagnostic. That way stale or unnecessary suppressions get cleaned up automatically instead of accumulating over time.

One more thing that would materially improve this: the results shouldn’t live as raw CI text hidden behind spoilers. It’s much more useful if clang-tidy shows up as an actual GitHub PR review with inline annotations and (where possible) apply-fix suggestions — like GitHub - ZedThree/clang-tidy-review: Create a pull request review based on clang-tidy warnings. That makes issues visible to all reviewers, reduces duplicate human comments, and makes it easier to have the right conversation on the borderline/noisy diagnostics (especially while some checks are still informational).

Finally, I’d make the rollout plan less open to interpretation with explicit steps:

  1. Fix the CI gating semantics for clang-tidy to use WarningsAsErrors (so clang-tidy returns non-zero only for enforced diagnostics), and have the calling code block based on exit status rather than “any output”.

  2. Add suppression auditing as part of the pipeline policy surface (this likely needs new work): implement a clang-tidy check or small companion linter that (a) forbids non-targeted NOLINT/NOLINTNEXTLINE and (b) errors on suppressions that don’t actually suppress any diagnostic.

  3. Enable clang-tidy as non-blocking in components that already have a .clang-tidy config, to validate infra/CI load and catch integration issues early.

  4. In parallel (component rollout):

    • Components with existing configs: promote low-FP checks to blocking where appropriate via WarningsAsErrors.
    • Components without configs: start with 1–few “always correct” checks (e.g. llvm-include-guard) and make those blocking from day one; then expand incrementally.
  5. Implement GitHub PR review reporting for clang-tidy diagnostics (parsing clang-tidy YAML output etc.), along the lines of GitHub - ZedThree/clang-tidy-review: Create a pull request review based on clang-tidy warnings.

  6. Once coverage is broad, move common config to the repo root to avoid divergence, using InheritParentConfig so components can override/add as needed.

Also, tracking this via GitHub issues would help: one issue per rollout step, and (where useful) issues per component–check (or component–check-family) so decisions are documented and discussion stays targeted. Many of those could be ‘good first issue’ candidates for incremental enablement work.

Stepping back, the main concern is keeping the behavior understandable over time: it should be obvious what is informational vs what is enforced, and that shouldn’t depend on CI log parsing or wrapper heuristics. Two things help a lot here: explicit enforcement rules and review-visible reporting. With those in place, the mix of blocking vs non-blocking checks can be tuned incrementally as we learn where false positives show up.

If someone wrote a large patch which used the wrong casing for variable names, for example, would you expect the review comments to be on every single instance? This could get incredibly noisy in the diff view and impair the readability of the PR for reviewers and others. If it’s just one comment per file, or similar, then I could definitely get behind this though.

Oh, I should also say that I don’t see a world where we make a clang-tidy check blocking, but don’t make clang-format/darker checks blocking first. Moreover, by blocking, I hope you mean “shows up as red, but you can still ignore it”, because even ignoring the need to be able to ignore false positives etc, I’m sure there are corner cases where it makes sense to ignore the check’s response.