ci: let clang-tidy-diff parse the two AUTOMOC self-including sources, and name option-coverage by its job id (fixes #624, fixes #637) - #647
Conversation
…637) linux-all-features' MORPH_BUILD_BANK_GUI rationale referred the reader to "the check-workflow-option-coverage job below". No job by that name exists in .github/workflows/: the job is `option-coverage`, and check_workflow_option_coverage.py is the script it runs. A reader who searches ci.yml for the name the comment gives finds nothing, and the sentence's whole purpose is to point at the standing guard. Measured on c55ea5b, before the change: $ grep -n 'check-workflow-option-coverage' .github/workflows/ci.yml 1838: # fetch requires. The check-workflow-option-coverage job below is $ grep -rn '^ check-workflow-option-coverage:' .github/workflows/ $ grep -n '^ option-coverage:' .github/workflows/ci.yml 2449: option-coverage: The replacement names both -- the job id a reader can jump to, and the script it runs -- so the next reader does not have to guess which of the two the sentence meant. Swept the rest of the tree for the same defect: over every `# ...` comment line in .github/workflows/*.yml, the hyphenated lowercase tokens immediately preceding the word "job" are automoc-include-lint, scenario-coverage, ladder-tests, linux-all-features, linux-coverage, option-coverage and dependency-free. All but the last resolve to a real job id in some workflow; "dependency-free job" is an adjective, not a reference. This was the only stale one. Nothing gates such a reference -- #638's check_workflow_job_banners.py pairs banners to jobs, and a job id inside a comment body is not a banner. That residual is accepted here and filed separately rather than folded in: closing it needs a new script plus the self-test this repository requires of a gate, which is its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
fixes #624) examples/common/testkit/test_qml_surface.cpp and src/qt/forms/tests/ tst_main.cpp both end with `#include "<own-basename>.moc"` -- the AUTOMOC idiom for a Q_OBJECT declared in a .cpp. AUTOMOC writes that header at build time, the clang-tidy job configures and never builds, so every PR that touches either file failed the job on a parse error before a single changed line was analysed. Reproduced locally on c55ea5b, this job's configure flags, clang-tidy 22.1.8, one line added to each of the two files: /.../examples/common/testkit/test_qml_surface.cpp:757:10: error: 'test_qml_surface.moc' file not found [clang-diagnostic-error] /.../src/qt/forms/tests/tst_main.cpp:70:10: error: 'tst_main.moc' file not found [clang-diagnostic-error] exit=1 ## Why generating, and not suppressing The issue offered two fixes. Suppressing the diagnostic turns out not to be one of them: clang-tidy does not let a compiler error be filtered. Measured on tst_main.cpp with the moc absent, all three still exit 1 on the same line -- --- baseline (no filter) : exit=1 'tst_main.moc' file not found --- -checks=-clang-diagnostic-error : exit=1 'tst_main.moc' file not found --- -checks=-clang-diagnostic-* : exit=1 'tst_main.moc' file not found --- -warnings-as-errors= empty : exit=1 'tst_main.moc' file not found so "filter it" would have meant grepping clang-tidy-diff.py's output and overriding its exit code -- morph#479's defect, a gate that cannot fail, rebuilt on purpose. That is what the rejected option would have cost. Generating them costs a partial build, measured locally with USE_COMPILER_CACHE=OFF on 12 cores: 100 ninja edges, 67 compilations, 7 links, 9 moc runs, 31s. 39 of the 67 are the vendored Lightweight ORM, pulled in because a <target>_autogen target depends on its target's link dependencies. Building *every* autogen target instead was measured at 264 objects and 179s, so the step names the two targets it needs. Compiler caching is still not wired into this job: 67 objects against the 703 a real build of this configure would compile. ## The comment the issue was really about ci.yml's "No Build step" paragraph already reasoned about which generated files exist without a build, answered it for configure_file() output, and read as though it had settled the question. It now says which kind it covers, and points at the AUTOMOC step for the kind it does not. The -Wno-missing-include-dirs comment made the matching over-broad claim -- that no source includes a generated header -- when what its guard covers is an ascending `moc_<name>.h`, not a same-name `.moc`; corrected too. ## Verification Measured locally on this commit's tree, from a clean configure-only build directory (`rm -rf build/clang-debug`, configure, 0 .moc files on disk), running the step body extracted from ci.yml rather than a retyped copy: === 2. NEGATIVE CONTROL: the step's own check, run before the build === ::error::examples/common/testkit/test_qml_surface.cpp: test_qml_surface.moc was not generated -- ... ::error::src/qt/forms/tests/tst_main.cpp: tst_main.moc was not generated -- ... ok: 2 self-included .moc header(s), 2 missing check-before-build exit=1 (must be 1) === 3. VACUITY CONTROL: same check with the scan finding nothing === ::error::no tracked source self-includes a .moc -- this step's scan has stopped detecting the idiom it exists for empty-scan exit=1 (must be 1) === 4. the step as written === ok: examples/common/testkit/test_qml_surface.cpp -> test_qml_surface.moc ok: src/qt/forms/tests/tst_main.cpp -> tst_main.moc ok: 2 self-included .moc header(s), 0 missing step exit=0 A green clang-tidy-diff would prove nothing on its own, so the job was made to report a finding on the changed line in each of the two files. A deliberate `int laneProbeReachMarker() { return 0; }` inserted above each `.moc` include, with the job's own clang-tidy-diff arguments: --- violation: clang-tidy-diff exit=1 /.../src/qt/forms/tests/tst_main.cpp:69:5: error: function 'laneProbeReachMarker' can be made static or moved into an anonymous namespace [misc-use-internal-linkage,...] /.../examples/common/testkit/test_qml_surface.cpp:756:5: error: function 'laneProbeReachMarker' can be made static or moved into an anonymous namespace [misc-use-internal-linkage,...] clang-diagnostic-error count: 0 --- harmless: clang-tidy-diff exit=0 clang-diagnostic-error count: 0 Both translation units are now analysed to completion and the changed line is reached; the same diff before this change produced two clang-diagnostic-errors and no findings at all. Not verified: any of this on a GitHub hosted runner, or against aqtinstall's Qt ${QT_VERSION} rather than the local distro Qt 6.11.2 and apt.llvm.org's clang-tidy-22 rather than local 22.1.8. The CI timings will be worse than 31s on four cores. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
Runner verificationThe ticket's framing was wrong and the lane was right to say so. #624 presented a trade-off between generating the AUTOMOC headers and filtering The control matters: my first attempt at this probe passed The step's two vacuity guards are in the script, not just in the report — read from the diff: if found == 0:
print("::error::no tracked source self-includes a .moc -- this "
"step's scan has stopped detecting the idiom it exists for")
sys.exit(1)
print(f"ok: {found} self-included .moc header(s), {missing} missing")
sys.exit(1 if missing else 0)A scan that stops matching the idiom fails instead of reporting green, and the list is derived from What I did not verify, and it is the branch's load-bearing claim. That the And the lane is right that CI cannot confirm it here. This PR's diff is #646 is the consequence worth flagging before merge: 84 findings become reachable (81 in #645 (nothing gates a prose job-id reference in a comment) records the #637 residual with the measurement showing why the cheap rule is not clean — two hits tree-wide, one of them a false positive, and no coverage of single-word job ids. Accepting with that recorded beats closing it by assertion. Not merged: CI incomplete. Next sweep counts the checks. 🤖 Generated with Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…uding TUs, and drop a deleted workflow from a docstring (fixes #646, fixes #643) (#653) * testkit/qt: clear the 84 clang-tidy findings the two AUTOMOC self-including TUs now report (fixes #646) #647 made the clang-tidy job build `ladder_common_tests_autogen` and `morph_forms_qml_tests_autogen`, so `examples/common/testkit/test_qml_surface.cpp` and `src/qt/forms/tests/tst_main.cpp` parse for the first time and are analysed. Nothing was broken -- clang-tidy-diff reports only changed lines -- but the first PR to touch one of those lines would have inherited findings that were not its own. Re-measured on 0067b5b with the clang-tidy job's own configure flags and clang-tidy 22.1.8 (CI's pinned major), both AUTOMOC targets built first: 81 findings in test_qml_surface.cpp (73 misc-const-correctness, 3 readability-convert-member-functions-to-static, 2 readability-inconsistent-declaration-parameter-name, 2 readability-identifier-length, 1 bugprone-easily-swappable-parameters) and 3 in tst_main.cpp. That reproduces the #647 lane's figure at c55ea5b exactly. 77 of the 84 are fixed rather than suppressed: * 73 `misc-const-correctness` -- local `QTemporaryDir` and fixture-bridge declarations that are never mutated. Applied with clang-tidy --fix, then rewritten to the west-const spelling the rest of the file uses. * 2 `readability-identifier-length` -- `id` -> `rowId`, `ok` -> `okay`. Safe: QmlSurfaceAudit reads `QMetaMethod::name()` and `parameterCount()` and never a parameter name, and the QML fixture text is unchanged. * 1 `misc-use-internal-linkage` -- `MorphFormsQmlTestSetup` moves into an anonymous namespace; QUICK_TEST_MAIN_WITH_SETUP expands in the same TU. * 1 `readability-redundant-access-specifiers` -- the explicitly defaulted default constructor and its `public:` are removed, which also removes the redundancy, since Q_OBJECT ends in `private:`. The remaining 7 get individually reasoned NOLINTNEXTLINEs, reason above the directive (#631/#627's rule) -- no NOLINT sweep and no new `.clang-tidy` entry, which is what #632 was about: * 4 `readability-convert-member-functions-to-static` on Q_PROPERTY readers, a Q_INVOKABLE and a Qt Quick Test setup slot. The reason is shape, not legality: the static form was measured to compile and moc registers the same property, but no bridge these fixtures stand in for has a static property reader, and a Qt slot is a member function by definition. * 2 `readability-inconsistent-declaration-parameter-name` on the two signals. moc's generated definitions name the parameters `_t1`/`_t2`, so no edit to the declarations can remove the mismatch; the finding reaches these files only because the classes are declared in a .cpp. * 1 `bugprone-easily-swappable-parameters` on the file-local `writeQml` helper's two adjacent `const QString&`. Verified, not asserted: * Both files are in this configure's compile_commands.json (703 entries, 695 in-workspace, 270 under examples/ -- above #649's 600/200 floors). * clang-tidy exits 0 on both files afterwards, and with every NOLINTNEXTLINE line stripped it exits 1 reporting exactly the 6 + 1 suppressed findings again. Each directive is load-bearing and the TUs are really analysed, rather than clean because nothing looked at them. * `ladder_common_tests "[qml-surface]"`: 182 assertions in 36 test cases, all passing. * `morph_forms_qml_tests -input src/qt/forms/tests`: 292 passed, 0 failed, the two corpus-reading suites among them -- which is what proves the setup slot still runs after the anonymous-namespace move. * clang-format 22.1.8 clean; check_nolint_directives.sh, check_bidi_controls.py, check_tidy_suppression_scope.sh, check_automoc_includes.sh, check_catch_test_names.sh all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW * ci: drop the deleted suppression-guard.yml from the banner gate's docstring (fixes #643) #635 deleted .github/workflows/suppression-guard.yml; the checker's "## Scope" paragraph still named it as one of the single-job workflows the gate skips. It was the last reference to that file in the tree: $ git grep -n "suppression-guard" -- . scripts/check_workflow_job_banners.py:50:suppression-guard.yml and the two wasm workflows are single-job files that have Gate behaviour was never affected and is not affected now -- the skip is derived per file, not read from that list. Measured on 0067b5b, over the five workflows with zero banners: ok: .github/workflows/docs.yml: no section banners, not in the banner style ok: .github/workflows/mutation.yml: no section banners, not in the banner style ok: .github/workflows/spec-sync.yml: no section banners, not in the banner style ok: .github/workflows/wasm-demo.yml: no section banners, not in the banner style ok: .github/workflows/wasm-ladder.yml: no section banners, not in the banner style ok: all 24 section banner(s) introduce the job they describe So the name is deleted rather than swapped for another: with it gone the sentence enumerates exactly the five files a run reports as skipped, and there is no sixth current example to put in its place. A second paragraph says so explicitly -- the list is an illustration with a shelf life, nothing reads it, and the run's own output is the current list -- so the next workflow deletion dates one sentence instead of producing a third round of this. Verified: `python3 scripts/check_workflow_job_banners.py .` and `bash scripts/test_check_workflow_job_banners.sh` both pass; the five skipped files and their single-job counts were enumerated from the tree with the checker's own BANNER_RE/JOB_KEY_RE rather than read off the docstring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tickets on the
ci.yml/ clang-tidy tree, one commit each.#637 —
ci.ymlnamed a job that does not existlinux-all-features'MORPH_BUILD_BANK_GUIrationale pointed the reader at "the check-workflow-option-coverage job below". The job isoption-coverage;check_workflow_option_coverage.pyis the script it runs. Reproduced onc55ea5b7:The replacement names both, so the next reader does not have to guess which of the two the sentence meant.
Swept for the same defect tree-wide. Over every
#comment line in.github/workflows/*.yml, the hyphenated lowercase tokens immediately preceding the word "job" areautomoc-include-lint,scenario-coverage,ladder-tests,linux-all-features,linux-coverage,option-coverageanddependency-free. All but the last resolve to a real job id somewhere in the tree;dependency-free jobis an adjective. This was the only stale one.The residual, decided explicitly: accepted, and filed
#638's
check_workflow_job_banners.pywould not have caught #637, and still would not. Its rule A inspects the line after a banner and rule B inspects whether a job has one; a job id inside a comment body is neither. So nothing gates this class.I am accepting that residual here rather than closing it, for two reasons:
test_check_workflow_option_coverage.sh,test_check_workflow_job_banners.sh), so closing it means a new script, a new self-test, and a new CI step. AGENTS.md is explicit that a finding gets filed and linked rather than folded in.valgrindorwindowsat all.Filed as #645, carrying that measurement.
#624 —
clang-tidy-diffcould not parse two translation unitsBoth sources end with
#include "<own-basename>.moc". AUTOMOC writes that header at build time; the job configures and never builds. Reproduced locally onc55ea5b7with the job's own configure flags and clang-tidy 22.1.8, one line added to each file:Which of the two fixes, and what the other would have cost
The ticket offered "restore AUTOMOC generation" against "filter
clang-diagnostic-errorfor these TUs" and said to pick one. The second one does not exist. clang-tidy does not let a compiler error be filtered — measured ontst_main.cppwith the moc absent:So "filter it" would have had to mean grepping
clang-tidy-diff.py's output and overriding its exit code — morph#479's defect, a gate that cannot fail, rebuilt on purpose. That is what the rejected option costs, and it is why this is not a judgement call between two tenable designs.What generating them costs, measured
The comment at the old
:2229argues against a build on caching grounds, so the cost was measured rather than assumed. On a 12-core Linux box withUSE_COMPILER_CACHE=OFF(the local default isfastcache-cc, which reported the same work as 3s and would have been a useless number):*_autogentarget (91)39 of the 67 are the vendored Lightweight ORM, pulled in because a
<target>_autogentarget depends on its target's link dependencies — that, not moc, is what the step costs. So the step names its two targets instead of building them all. Compiler caching stays unwired: 67 objects against the 703 a real build of this configure would compile.A four-core hosted runner will be slower than 31s. That is inferred, not measured.
Both comments updated, as the ticket required
configure_file()output while reading as though it had settled the question. It now says which kind it covers and points at the AUTOMOC step for the kind it does not, and reads "no full Build step".-Wno-missing-include-dirscomment made the matching over-broad claim — that no source underexamples/,include/orsrc/includes a generated header. What the "Check no generated moc include ascends" guard covers is an ascendingmoc_<name>.h; a same-name.mocin the target's own autogen dir is a different shape and the flag says nothing about it, because the error is the file being absent, not the directory. Corrected.The list is written out; the check is derived
Two target names are spelled out in the step, so they are greppable and reviewable. The check underneath rescans every tracked C++ source for the self-include idiom and fails if a
.moca source names is still missing — which is what a third source adopting the idiom looks like — and fails if it finds no self-include at all.Verification
ci.ymlis the only file this branch touches. The safety of #624 rests on one claim: theclang-tidyjob now analyses both translation units to completion and reaches the changed line. A greenclang-tidy-diffwould not show that, so it was made to go red on a line in each file.int laneProbeReachMarker() { return 0; }inserted above each.mocinclude, run with the job's ownclang-tidy-diff.pyarguments:A finding reported at
tst_main.cpp:69andtest_qml_surface.cpp:756— the changed lines — is the proof; the same diff before this change produced twoclang-diagnostic-errors and no findings at all. Both probe edits were reverted; neither source file is modified by this branch (test_qml_surface.cppis held by a concurrent lane and was only read and probed).The step's own controls, from a clean configure-only tree (
rm -rf build/clang-debug, configure, 0.mocon disk), running the step body extracted fromci.ymlrather than a retyped copy:Gates run locally after the edits, all green:
.github/workflows/ci.ymlalso parses as YAML after the edit — worth stating because the first draft of the new step was namedGenerate the AUTOMOC headers two sources #include directly, and YAML silently truncated the name at the unquoted#. Caught by parsing the file and printing the step names back.Not verified
aqtinstall'sQT_VERSION; local clang-tidy is 22.1.8, not apt.llvm.org'sclang-tidy-22; localclang++stands in forclang++-22..mocfrom a target this configure does not enable — that path prints a::warning::and continues, by design, since clang-tidy cannot analyse such a file either.Findings filed, not folded
test_qml_surface.cpp, 3 intst_main.cpp) become reachable once this lands, because no check has ever run over either file. Nothing breaks today —clang-tidy-diffonly reports changed lines — but the first PR to touch one of those lines will fail on debt that is not its own.🤖 Generated with Claude Code
https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW