From 0ae24f75368ba90e2528b4e29d6a548644edcfae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Oct 2026 16:00:41 -1000 Subject: [PATCH] [core] Catch heap allocating helper calls with explicit template arguments (#20391) --- esphome/components/debug/debug_zephyr.cpp | 18 +++++++++--------- script/ci-custom.py | 3 ++- tests/script/test_ci_custom.py | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index cec2ab0be8..f3a5f3d770 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -1,5 +1,6 @@ #include "debug_component.h" #ifdef USE_ZEPHYR +#include #include #include "esphome/core/alloc_helpers.h" #include "esphome/core/log.h" @@ -400,18 +401,17 @@ size_t DebugComponent::get_device_info_(std::span #endif } #endif - auto uicr = [](volatile uint32_t *data, uint8_t size) { - std::string res; + // 13 words of 8 hex digits, separated by spaces + char uicr_buf[13 * 9]; + auto uicr = [&uicr_buf](volatile uint32_t *data, uint8_t size) { + size_t len = 0; for (size_t i = 0; i < size; i++) { - if (i > 0) { - res += ' '; - } - res += format_hex_pretty(data[i], '\0', false); + len = buf_append_printf(uicr_buf, sizeof(uicr_buf), len, i > 0 ? " %08" PRIX32 : "%08" PRIX32, data[i]); } - return res; + return uicr_buf; }; - ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); - ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13)); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12)); #ifdef ESPHOME_LOG_HAS_VERBOSE log_peripherals_info(); #endif diff --git a/script/ci-custom.py b/script/ci-custom.py index 7839038a8c..8361387af0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1065,7 +1065,8 @@ HEAP_ALLOCATING_HELPERS = { r"str_sprintf|" r"str_snprintf|" r"value_accuracy_to_string" - r")\s*\(" + CPP_RE_EOL, + # Explicit template arguments at any nesting depth, e.g. format_hex>( + r")\s*(?:<[^;{}()\n]*>\s*)?\(" + CPP_RE_EOL, include=cpp_include, exclude=[ # The definitions themselves diff --git a/tests/script/test_ci_custom.py b/tests/script/test_ci_custom.py index 998650079e..f303d19bb0 100644 --- a/tests/script/test_ci_custom.py +++ b/tests/script/test_ci_custom.py @@ -444,6 +444,28 @@ def test_log_tag_as_string_ignores_logging(line: str) -> None: assert not ci_custom.lint_log_tag_as_string("test.cpp", line + "\n") +@pytest.mark.parametrize( + ("content", "flagged"), + [ + ("auto s = format_hex_pretty(data, len);\n", True), + # Explicit template arguments must not hide the call + ("auto s = format_hex_pretty(v, '\\0', false);\n", True), + ("auto s = format_hex>(arr);\n", True), + ( + "auto s = format_hex, uint64_t>>(v);\n", + True, + ), + ("auto s = format_hex < 3;\n", False), + ("format_hex_pretty_to(buf, sizeof(buf), data, len);\n", False), + ], +) +def test_heap_allocating_helpers_detection(content: str, flagged: bool) -> None: + errs = ci_custom.lint_no_heap_allocating_helpers( + Path("esphome/components/x/x.cpp"), content + ) + assert bool(errs) is flagged + + # --- rule: every component __init__.py defines DOMAIN as its own name ---