Skip to content

clang-tidy only ever sees changed lines: no job lints unchanged first-party code, and the standing bill is 4155 findings #677

Description

@Yaraslaut

The gap

ci.yml's clang-tidy-diff is the only job in this repository that runs clang-tidy over the tree, and it is diff-scoped twice over: clang-tidy-diff.py analyses only the files named in the diff, and passes a -line-filter that drops every finding outside the diff's own line ranges.

The consequence is that a finding in a first-party file nobody has touched is reported by no job at all, and never becomes reportable until someone happens to edit that exact line. That is not a bug in the diff job — a whole-file -Werror lint over this tree could not be turned on in one step — but it is a coverage gap that nothing currently records, and it is the gap #664 was pointing at from one directory over. (#664 blamed the root HeaderFilterRegex; measured, that regex has no observable effect on this job at all — see the correction on #664.)

The bill, measured rather than estimated

So the question a whole-file leg has to answer is "how much is standing debt". I measured it while working #664, on 563502ab, clang 22.1.8, Qt 6.11.2, with the clang-tidy job's own configure flags from a cold build directory:

cmake --preset clang-debug -DMORPH_BUILD_NET=ON -DMORPH_BUILD_QT=ON \
  -DMORPH_BUILD_FORMS_QML=ON -DMORPH_BUILD_OFFLINE_SQLITE=ON \
  -DMORPH_BUILD_LOAD_TESTS=ON -DMORPH_BUILD_HMAC_EXAMPLES=ON \
  -DMORPH_BUILD_FUZZERS=ON -DMORPH_BUILD_LADDER=ON \
  -DMORPH_BUILD_BANK_EXAMPLE=ON -DMORPH_BUILD_BANK_GUI=ON

740 database entries, 423 distinct in-workspace translation units, 282 under examples/. clang-tidy was then run over all 423, --header-filter='.*' --extra-arg=-std=c++23 --extra-arg=-Wno-missing-include-dirs, one output file per TU, findings de-duplicated by (file, line, column, check):

distinct findings under --header-filter='.*' : 5577
  of which in a main file (always shown)     : 3010
  of which in an included header             : 2567

  by bucket:
     2930  first-party: examples/
     1422  build tree (_deps, vendored, generated)
      713  first-party: tests/
      488  first-party: include/
       22  first-party: src/
        2  system/toolchain

So a whole-file leg over first-party code, with the vendored trees excluded, starts at roughly 4155 findings (5577 − 1422 vendored), of which 3010 are in .cpp main files and ~657 in first-party headers that today's HeaderFilterRegex also excludes. For scale: #646 was 84 findings and #656 was 97.

Headers only, by root:

      633 findings in  150 header(s)  examples/
      488 findings in   34 header(s)  include/
       21 findings in    6 header(s)  tests/
        1 findings in    1 header(s)  src/

Cost, measured on a 12-core Linux box: 423 TUs at -P 10 took ~20 minutes wall, the heaviest TUs dominated by include/morph/forms/forms.hpp (102 findings alone) and the ORM-bearing bank models. A four-core hosted runner pays more.

What a fix would have to settle, and two facts that constrain it

  1. Vendored code must be excluded explicitly, and an inclusive first-party regex will not do it. A header filter is an unanchored search over an absolute path, and (include/morph|src|tests|examples)/ matches 608 headers under build/clang-debug/_deps — 244 glaze, 202 stdexec, 153 Lightweight — because FetchContent lays dependencies out as _deps/<name>-src/src/ and _deps/<name>-src/include/. The polarity that works is HeaderFilterRegex: ".*" with an ExcludeHeaderFilterRegex naming the build tree (/_deps/, /build/, _autogen/, /CMakeFiles/); it also cannot go stale when a rung is added, because it names nothing first-party. Verified behaviourally: on one bank model TU that exclusion takes vendored diagnostic lines from 1300 to 0.
  2. A whole-file leg cannot simply be WarningsAsErrors: "*" on day one. 4155 findings is not a backlog anyone clears in a PR, and suppressing to green is the outcome this repository has filed issues about repeatedly. The plausible shapes are a non-blocking reporting leg, a ratchet on the count, or a per-directory opt-in — all of which are design decisions, not implementation ones, which is why this is filed rather than attempted.

Verification status

  • Reproduced: every number above, on 563502ab, over 423 of 423 in-workspace TUs — a full sweep, not a sample. The 608-vendored-header figure is a direct os.walk over build/clang-debug/_deps matched against the regex. The 1300→0 exclusion figure is two clang-tidy runs on examples/bank/src/models/account_model.cpp.
  • Not verified: the wall-clock cost on a CI runner (measured locally only); whether the 4155 figure is stable across clang minor versions; and whether any of the findings are false positives worth suppressing rather than fixing — I counted them, I did not read them.
  • Weak: the "roughly 4155" figure subtracts the vendored bucket from the total, which assumes the exclusion above is what a real leg would use. With a different exclusion the number moves.

What would change the verdict

Close as invalid if a job is added that already lints unchanged first-party code and I missed it — I checked .github/workflows/* and scripts/* for clang-tidy and run-clang-tidy invocations and found only the diff job plus scripts/check_tidy_suppression_scope.sh, which drives a synthetic probe rather than the tree. Close as wontfix if the considered answer is that diff-scoped lint is the intended coverage level — in which case this issue is still worth keeping as the record of what that choice costs, because nothing else in the repository writes it down.

Found while working #664 (PR #676). Filed, not folded.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ciSubsystem: cibugSomething isn't workingenhancementNew feature or requesttriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions