Skip to content

Run clang-tidy in CI, matching upstream ESPHome's check list - #91

Merged
nuttytree merged 1 commit into
masterfrom
chore/clang-tidy-in-ci
Aug 5, 2026
Merged

nuttytree merged 1 commit into
masterfrom
chore/clang-tidy-in-ci

Conversation

@nuttytree

Copy link
Copy Markdown
Owner

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.

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.

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>
@nuttytree
nuttytree merged commit dabcf7d into master Aug 5, 2026
7 checks passed
@nuttytree
nuttytree deleted the chore/clang-tidy-in-ci branch August 5, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant