Repository navigation
Run clang-tidy in CI, matching upstream ESPHome's check list - #91
Merged
Merged
Conversation
The compile jobs added in #88 prove the custom components still build, but a build only catches what the compiler rejects. Upstream ESPHome runs clang-tidy over every C++ source with a large check list; nothing here did, so the components drifted from the conventions and analysis ESPHome core is held to. Adopting it is not just a matter of pointing clang-tidy at components/. clang cannot consume the xtensa/riscv GCC command line the real build uses, and the components only compile against a device's generated esphome/core/defines.h. Upstream solves both with dedicated "tidy" PlatformIO environments; this repo has no platformio.ini of its own, so the flags come from an actual device build via `esphome idedata` -- which the CI compile matrix already produces. Changes: - Add .clang-tidy, a verbatim copy of upstream's, so a component here is held to exactly the checks it would face in ESPHome core. - Add scripts/clang_tidy.py, a slimmed-down port of upstream's script/clang-tidy: it reads the compiled build's idedata, rebuilds the clang command line from it (dropping GCC-only flags, macro-replacing the Arduino/newlib headers that use GNU extensions), and checks only the components that device built, in parallel. - Run it in the existing compile matrix, right after each `esphome compile`, so it costs no extra build. A component that runs on both platforms is analyzed once per platform, same as the compile itself. Register a problem matcher so findings annotate the diff. - Two adaptations upstream does not need. It analyzes at ESPHOME_LOG_LEVEL_VERY_VERBOSE, because these devices build with `log_level: none`, which preprocesses every ESP_LOG* call away -- hiding the code inside them and making log-only variables look unused. And it passes -fgnuc-version=4.2.1, which is what clang already assumes on Linux; on Windows it defaults to an MSVC target that leaves __GNUC__ undefined, and newlib's __GNUC_PREREQ ladders then fall back to `throw()` exception specifications, a hard error under C++17. CI is unaffected either way. The new checks found real problems in two components, fixed here so CI lands green: - pool_controller: reset_all_pump_runtimes_() logged this->last_check_->hour with no has_value() guard at all; it now takes the boundary time as a parameter, which also makes it explicit that the value is the walked time while catching up, not the current clock. The catch-up loop's own accesses are unprovable across calls clang-tidy cannot see into, so it binds the contained value once under the guard. - econet_zone_control: update_zone_fan_mode_() dereferenced min_zone/max_zone unconditionally after the scan, which is null if no zones are configured. The schema requires at least two, so this is unreachable today; guarded rather than left to depend on a validator elsewhere. - pool_controller: eight protected methods in pump_switch.h now carry the trailing underscore ESPHome uses for non-public members, which the file already used for others. Renamed by hand: clang-tidy's --fix renamed has_current_sensor() inside `#ifdef USE_SENSOR` and left the `#else` definition on the old name, which builds here and breaks a device without sensors. - Concatenated nested namespaces across pool_controller, and added braces to two multi-line statement bodies. - The one finding not fixed is a bugprone-branch-clone in update_current_action_(): "Off" is tested first and deliberately shares the fallback's body so it wins over a state string containing another keyword. Suppressed with a NOLINT and the reason, since the datapoint's value set is not documented anywhere. Verified by compiling all five devices in the CI matrix from scratch copies and running the new script against each: every component passes. Co-Authored-By: Claude Opus 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.
The compile jobs added in #88 prove the custom components still build, but a build only catches what the compiler rejects. Upstream ESPHome runs clang-tidy over every C++ source with a large check list; nothing here did, so the components drifted from the conventions and analysis ESPHome core is held to.
Adopting it is not just a matter of pointing clang-tidy at
components/. clang cannot consume the xtensa/riscv GCC command line the real build uses, and the components only compile against a device's generatedesphome/core/defines.h. Upstream solves both with dedicated "tidy" PlatformIO environments; this repo has noplatformio.iniof its own, so the flags come from an actual device build viaesphome idedata— which the CI compile matrix already produces.Changes:
.clang-tidy, a verbatim copy of upstream's, so a component here is held to exactly the checks it would face in ESPHome core.scripts/clang_tidy.py, a slimmed-down port of upstream'sscript/clang-tidy: it reads the compiled build's idedata, rebuilds the clang command line from it (dropping GCC-only flags, macro-replacing the Arduino/newlib headers that use GNU extensions), and checks only the components that device built, in parallel.esphome compile, so it costs no extra build. A component that runs on both platforms is analyzed once per platform, same as the compile itself. Register a problem matcher so findings annotate the diff.ESPHOME_LOG_LEVEL_VERY_VERBOSE, because these devices build withlog_level: none, which preprocesses everyESP_LOG*call away — hiding the code inside them and making log-only variables look unused. And it passes-fgnuc-version=4.2.1, which is what clang already assumes on Linux; on Windows it defaults to an MSVC target that leaves__GNUC__undefined, and newlib's__GNUC_PREREQladders then fall back tothrow()exception specifications, a hard error under C++17. CI is unaffected either way.The new checks found real problems in two components, fixed here so CI lands green:
reset_all_pump_runtimes_()loggedthis->last_check_->hourwith nohas_value()guard at all; it now takes the boundary time as a parameter, which also makes it explicit that the value is the walked time while catching up, not the current clock. The catch-up loop's own accesses are unprovable across calls clang-tidy cannot see into, so it binds the contained value once under the guard.update_zone_fan_mode_()dereferencedmin_zone/max_zoneunconditionally after the scan, which is null if no zones are configured. The schema requires at least two, so this is unreachable today; guarded rather than left to depend on a validator elsewhere.pump_switch.hnow carry the trailing underscore ESPHome uses for non-public members, which the file already used for others. Renamed by hand: clang-tidy's--fixrenamedhas_current_sensor()inside#ifdef USE_SENSORand left the#elsedefinition on the old name, which builds here and breaks a device without sensors.pool_controller, and added braces to two multi-line statement bodies.bugprone-branch-cloneinupdate_current_action_():"Off"is tested first and deliberately shares the fallback's body so it wins over a state string containing another keyword. Suppressed with a NOLINT and the reason, since the datapoint's value set is not documented anywhere.No behaviour changes on upgrade: the C++ edits are internal renames, guards on unreachable paths, and a log argument that resolves to the same value at every call site. No persisted state is affected.
Verified by compiling all five devices in the CI matrix from scratch copies (
esphome compile) and running the new script against each build: every component passes.