Enforce clang-tidy, at a version that cannot move under us - #99
Merged
Conversation
The repository has maintained a .clang-tidy and 13 NOLINT comments in the header for years without ever running clang-tidy in CI. The config said Checks: "*", which enables every check any future release adds, so wiring it up naively would have meant a red build on every toolchain bump for reasons that have nothing to do with a change. That is a curation problem, not a missing workflow, so this does the curation. Families are now listed explicitly, with the ones that are wrong for this code turned off and a line each saying why. The version macros cannot be enums because the preprocessor builds the inline namespace name out of them; storage keeps its elements in a trailing array on purpose; the one enum is a template tag that is never stored. Families still grow, so the actual pin is the clang-tidy version. Pinning only the compiler turned out not to be a pin at all: clang-tidy parses the code with whatever standard library is installed, and clang-tidy-18 against Fedora's libstdc++ 16 dies inside <string>. So the default is a container image, which pins both halves, with SVECTOR_CLANG_TIDY to override it where the environment is known to be consistent. CI sets it to an apt-installed clang-tidy-18, which keeps the lint job quick and off Docker Hub, whose anonymous pull limit is shared across runners and would make a required check flaky. svector is a template, so a check only sees what something instantiated. The generated translation unit exercises a broad spread of the API for that reason: with only a push_back, three quarters of the container is never looked at, and the assign and insert findings below do not appear at all. Of what it found, one was worth fixing: enable_if_t was still spelled the C++11 way. The rest are deliberate, and are now NOLINTs that say so. Three are worth knowing about, because doing what the check asks would put back a bug that has already been fixed once: * performance-unnecessary-copy-initialization on the copy in assign, resize and insert. That copy is what makes v.resize(1000, v[0]) work, see issue #50. * cppcoreguidelines-missing-std-forward on resize_after_reserve. Its pack goes to a fill that copies it into every new element; forwarding would move from it on the first one and leave the rest with a moved-from value. * misc-no-recursion on the same three functions, which re-dispatch exactly once after copying the aliasing value out of the way. The rest are the tagged pointer being itself: sizeof(ptr) really does mean the pointer, and the scope guards really are meant not to be movable. Verified the check fails on a planted violation, not just that it passes. Closes #88. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #88.
The problem was curation, not a missing workflow
This repository has maintained a
.clang-tidyand 13NOLINTcomments in the header for years without ever running clang-tidy in CI. The config saidChecks: "*", which enables every check any future release adds, so wiring it up naively would have meant a red build on every toolchain bump for reasons unrelated to any change. So this does the curation first.Families are now listed explicitly, and the ones that are wrong for this code are off with a line each saying why — the version macros cannot be enums because the preprocessor builds the inline namespace name out of them,
storagekeeps its elements in a trailing array on purpose, the one enum is a template tag that is never stored.Pinning the compiler turned out not to be a pin
Families still grow, so the real guarantee is the clang-tidy version. But pinning only the compiler is not enough: clang-tidy parses the code with whatever standard library is installed, and I hit this immediately —
clang-tidy-18against Fedora's libstdc++ 16 dies inside<string>.So the default is a container image, which pins both halves, with
SVECTOR_CLANG_TIDYto override where the environment is known to be consistent. CI sets it to an apt-installedclang-tidy-18, which keeps the lint job quick and off Docker Hub — whose anonymous pull limit is shared across GitHub's runners and would make a required check flaky. With neither available the check skips loudly rather than failing, solint-all.pystays usable.A template only gets checked where it is instantiated
svectoris a template, so a check only sees what something instantiated. The generated TU deliberately exercises a broad spread of the API: with only apush_back, three quarters of the container is never looked at, and theassignandinsertfindings below do not appear at all.What it found
One thing worth fixing:
enable_if_twas still spelled the C++11 way. Fixed.Everything else is deliberate and is now a
NOLINTthat says so. Three are worth knowing about, because doing what the check asks would put back a bug that has already been fixed once:performance-unnecessary-copy-initializationon the copy inassign,resizeandinsert. That copy is what makesv.resize(1000, v[0])work — issue v.push_back(v[0]) doesn't work, can lead to segmentation fault on resizing #50.cppcoreguidelines-missing-std-forwardonresize_after_reserve. Its pack goes to a fill that copies it into every new element; forwarding would move from it on the first element and leave the rest with a moved-from value.misc-no-recursionon those same three functions, which re-dispatch exactly once after copying the aliasing value out of the way.The rest are the tagged pointer being itself:
sizeof(ptr)really does mean the pointer, and the scope guards really are meant not to be movable — their copies are= delete, which already suppresses the implicit moves, and a guard you can move out of its scope is not doing its job.The issue asked me to look properly at
missing-std-forwardandstorage_guard's special members as "plausibly substantive". Both are correct as written, and the first would be an outright bug if changed.Verified
enable_if_tchange.lint-all.pyis green, and picks the new linter up automatically through itslint-*glob.newtriggered nothing, becausecppcoreguidelines-owning-memoryis deliberately off.Note
The one thing CI cannot tell you is whether the container path works, since CI takes the override. I ran both locally: container clean, override reproducing the libstdc++ 16 incompatibility described above.
🤖 Generated with Claude Code