Skip to content

tests/.clang-tidy's suppressions also silence include/morph headers, so a header gate driven by test TUs under-reports #632

Description

@Yaraslaut

Summary

tests/.clang-tidy disables thirteen checks, each with a documented reason about
test idiom. But clang-tidy applies the configuration of the main translation
unit
, not of the file a diagnostic lands in. So every one of those thirteen
suppressions also applies to include/morph/** headers, whenever the header is
reached from a TU under tests/.

That means a header finding's visibility depends on which TU happened to reach
it. The same finding in the same header is reported from an examples/ TU and
silently dropped from a tests/ TU.

This matters now for two reasons:

  1. clang-tidy-diff's changed-lines scope hides 101 findings in include/morph/, each a trap for the next edit to that line #580's census gate proposal is directly affected. That census recommends
    gating on "all 60 [verify_interface_header_sets] stubs plus every TU under
    tests/"
    . The tests/ half of that gate cannot report
    cppcoreguidelines-pro-bounds-avoid-unchecked-container-access — the check
    that is 250 of the census's 596 — in any header it reaches, nor
    bugprone-unchecked-optional-access (4 of the census's flagged possible
    defects), nor readability-identifier-length (58). A gate built to that
    recipe would be green on findings it was built to catch.
  2. It is a second instance of this repository's named failure mode — a
    control that reports success while measuring nothing — and it is invisible
    from inside: tests/.clang-tidy is a correct and well-argued file about
    tests/, and nothing in it says it reaches further.

Verification status: reproduced

Revision 0e3b8823 (origin/master), clang-tidy 22.1.8, .clang-tidy
unmodified, compile database configured exactly as .github/workflows/ci.yml's
clang-tidy job configures it (ci.yml:2191, every optional feature ON).

The case is include/morph/forms/forms.hpp:496, which #627 records as leaking
two cppcoreguidelines-pro-bounds-avoid-unchecked-container-access findings past
a wrapped NOLINTNEXTLINE. It is inside forEachNamedMember, a template, so it
needs a TU that instantiates it.

From a tests/ TU that does instantiate it —
tests/test_views.cpp, whose own comments say it "exercises buildViewSchema's
forEachNamedMember walk"
:

$ clang-tidy -p build/clang-debug --quiet --extra-arg=-std=c++23 \
    --extra-arg=-Wno-missing-include-dirs tests/test_views.cpp
$ grep -c "unchecked-container-access" test_views.log
0

Zero, across the whole run — not just at :496. The check is off for the entire
include closure.

Other checks from the same TU are unaffected, so the run was real:

$ grep -E "forms\.hpp:(489|496)" test_views.log | sort -u
.../include/morph/core/../forms/forms.hpp:489:39: error: forwarding reference parameter 'action' is never forwarded inside the function body [cppcoreguidelines-missing-std-forward,-warnings-as-errors]
.../include/morph/core/../forms/forms.hpp:489:57: error: forwarding reference parameter 'visitor' is never forwarded inside the function body [cppcoreguidelines-missing-std-forward,-warnings-as-errors]

From a non-tests/ TU, examples/forms/gui_qml/FormsController.cpp, same
revision, same database, same command:

forms/forms.hpp:496:41: error: possibly unsafe 'operator[
forms/forms.hpp:496:66: error: possibly unsafe 'operator[

Both findings present.

tests/.clang-tidy carries InheritParentConfig: true and this in its
Checks: list, among twelve others:

  -bugprone-unchecked-optional-access,
  -cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
  -readability-identifier-length,
  -readability-function-cognitive-complexity,
  -readability-convert-member-functions-to-static,
  ...

Its own comment for that entry scopes the reasoning to test code — "these are
dom["key"] on the line after REQUIRE(dom.contains("key")) … the bound is
established by the assertion the check cannot read"
— which is a correct
statement about tests/ and says nothing about include/morph/. The file's
header likewise says "Checks suppressed inside tests/ only", which is what
the author intended and not what clang-tidy does.

The same applies to the nine examples/*/tests/.clang-tidy copies, which carry
the same entries.

What I did not verify

  • I did not measure how many of clang-tidy-diff's changed-lines scope hides 101 findings in include/morph/, each a trap for the next edit to that line #580's 596 findings this hides from a
    tests/-driven run.
    Establishing that means re-running the census twice —
    once over tests/ TUs only, once over the rest — and differencing. The census
    itself was a full-tree sweep over 695 TUs, so its 596 is not affected; only
    a gate built from a tests/-TU subset would be.
  • I did not check whether clang-tidy has an option to apply the diagnostic
    file's config rather than the main file's.
    I believe it does not (the config
    is resolved once, from the TU path, before analysis), but I read no clang-tidy
    source.
  • Not reproduced on the CI image, only locally on Arch Linux with clang-tidy
    22.1.8.
  • This is not a claim that tests/.clang-tidy is wrong. Every entry in it is
    argued and, for test code, convincing. The defect is the unstated reach.

What would change the verdict

Close this when one of:

  • the header gate contemplated by clang-tidy-diff's changed-lines scope hides 101 findings in include/morph/, each a trap for the next edit to that line #580 is built in a way that does not depend on
    a tests/-TU's configuration — e.g. gating only on the
    verify_interface_header_sets stubs, which sit under build/ and inherit the
    root .clang-tidy, plus non-tests/ user TUs for the instantiation-dependent
    residual; or
  • tests/.clang-tidy's reach is narrowed so its suppressions cannot apply to
    include/morph/** (if clang-tidy offers a mechanism for that), or
  • it is written down — in tests/.clang-tidy's own header and wherever the
    header gate is documented — that these thirteen checks are unreportable for
    any header reached from a test TU, with the list of which ones.

Re-open if a gate is later built on tests/ TUs without that record.

Related

Found while verifying #627's fixes in PR #631. Filed rather than folded in, per
AGENTS.md: it is about the check configuration's scope, not about any of the
directives that PR repairs.

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 workingtriage: 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