From 971a1a3e00eeb3f657cbf5a22bd59c4c20ad4e89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 08:49:31 -1000 Subject: [PATCH 1/3] [ci] Block new std::to_string() usage, suggest snprintf alternatives --- .../components/api/homeassistant_service.h | 4 +- esphome/components/esp32_ble/ble_uuid.h | 2 +- esphome/components/shtcx/shtcx.cpp | 2 +- .../voice_assistant/voice_assistant.h | 2 +- script/ci-custom.py | 41 +++++++++++++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9bffe18764..aa09d1abcc 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -25,7 +25,9 @@ template class TemplatableStringValue : public TemplatableValue static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } + template static std::string value_to_string(T &&val) { + return to_string(std::forward(val)); + } // NOLINT // Overloads for string types - needed because std::to_string doesn't support them static std::string value_to_string(char *val) { diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index ae593955a4..ac7ad42240 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -46,7 +46,7 @@ class ESPBTUUID { esp_bt_uuid_t get_uuid() const; - std::string to_string() const; + std::string to_string() const; // NOLINT const char *to_str(std::span output) const; protected: diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 933dd9bde9..5d5fbf1740 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -52,7 +52,7 @@ void SHTCXComponent::dump_config() { ESP_LOGCONFIG(TAG, "SHTCx:\n" " Model: %s (%04x)", - to_string(this->type_), this->sensor_id_); + to_string(this->type_), this->sensor_id_); // NOLINT LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index b1b3df7bbd..7daf84454a 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -81,7 +81,7 @@ struct Timer { this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, YESNO(this->is_active)); return buffer.data(); } - std::string to_string() const { + std::string to_string() const { // NOLINT char buffer[TO_STR_BUFFER_SIZE]; return this->to_str(buffer); } diff --git a/script/ci-custom.py b/script/ci-custom.py index ee2e73872c..8e08ba4095 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -732,6 +732,47 @@ def lint_no_heap_allocating_helpers(fname, match): ) +@lint_re_check( + # Match std::to_string() or unqualified to_string() calls + # The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string + # Use negative lookbehind to avoid matching: + # - Function definitions: "const char *to_string(" or "std::string to_string(" + # - Method definitions: "Class::to_string(" + # - Method calls: ".to_string(" or "->to_string(" + # - Other identifiers: "_to_string(" + r"(?:])to_string\s*\(" + CPP_RE_EOL, + include=cpp_include, + exclude=[ + # Vendored library + "esphome/components/http_request/httplib.h", + # Deprecated helpers that return std::string + "esphome/core/helpers.cpp", + # The using declaration itself + "esphome/core/helpers.h", + # Test fixtures - not production embedded code + "tests/integration/fixtures/*", + ], +) +def lint_no_std_to_string(fname, match): + return ( + f"{highlight('std::to_string()')} allocates heap memory. On long-running embedded " + f"devices, repeated heap allocations fragment memory over time.\n" + f"Please use {highlight('snprintf()')} with a stack buffer instead.\n" + f"\n" + f"Buffer sizes and format specifiers:\n" + f" uint8_t/int8_t: 4 chars - %u / %d (or PRIu8/PRId8)\n" + f" uint16_t/int16_t: 6 chars - %u / %d (or PRIu16/PRId16)\n" + f" uint32_t/int32_t: 11 chars - %" + "PRIu32 / %" + "PRId32\n" + " uint64_t/int64_t: 21 chars - %" + "PRIu64 / %" + "PRId64\n" + f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n" + f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n" + f"\n" + f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n" + f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n' + f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)" + ) + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 11fb46ad11281725ba7e7020b5d0c6296865c4fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:44:25 -1000 Subject: [PATCH 2/3] Apply suggestions from code review --- esphome/components/api/homeassistant_service.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index aa09d1abcc..d8a12a879c 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -26,8 +26,8 @@ template class TemplatableStringValue : public TemplatableValue static std::string value_to_string(T &&val) { - return to_string(std::forward(val)); - } // NOLINT + return to_string(std::forward(val)); // NOLINT + } // Overloads for string types - needed because std::to_string doesn't support them static std::string value_to_string(char *val) { From 6ca1b907520309e282a9b4eea4046b4c31c1b247 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 18:14:03 -1000 Subject: [PATCH 3/3] Address Copilot review feedback - Fix regex to actually match std::to_string() by using alternation (the : in the lookbehind was preventing matches) - Update error message to mention both std::to_string() and unqualified to_string() forms - Correct buffer sizes for signed integer types: - int8_t: 5 chars (not 4) for "-128\0" - int16_t: 7 chars (not 6) for "-32768\0" - int32_t: 12 chars (not 11) for "-2147483648\0" --- script/ci-custom.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/script/ci-custom.py b/script/ci-custom.py index d436c25b60..864279cab8 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -759,12 +759,13 @@ def lint_no_sprintf(fname, match): @lint_re_check( # Match std::to_string() or unqualified to_string() calls # The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string - # Use negative lookbehind to avoid matching: + # Use negative lookbehind for unqualified calls to avoid matching: # - Function definitions: "const char *to_string(" or "std::string to_string(" # - Method definitions: "Class::to_string(" # - Method calls: ".to_string(" or "->to_string(" # - Other identifiers: "_to_string(" - r"(?:])to_string\s*\(" + CPP_RE_EOL, + # Also explicitly match std::to_string since : is in the lookbehind + r"(?:(?:])to_string|std\s*::\s*to_string)\s*\(" + CPP_RE_EOL, include=cpp_include, exclude=[ # Vendored library @@ -779,15 +780,20 @@ def lint_no_sprintf(fname, match): ) def lint_no_std_to_string(fname, match): return ( - f"{highlight('std::to_string()')} allocates heap memory. On long-running embedded " - f"devices, repeated heap allocations fragment memory over time.\n" + f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) " + f"allocates heap memory. On long-running embedded devices, repeated heap allocations " + f"fragment memory over time.\n" f"Please use {highlight('snprintf()')} with a stack buffer instead.\n" f"\n" - f"Buffer sizes and format specifiers:\n" - f" uint8_t/int8_t: 4 chars - %u / %d (or PRIu8/PRId8)\n" - f" uint16_t/int16_t: 6 chars - %u / %d (or PRIu16/PRId16)\n" - f" uint32_t/int32_t: 11 chars - %" + "PRIu32 / %" + "PRId32\n" - " uint64_t/int64_t: 21 chars - %" + "PRIu64 / %" + "PRId64\n" + f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n" + f" uint8_t: 4 chars - %u (or PRIu8)\n" + f" int8_t: 5 chars - %d (or PRId8)\n" + f" uint16_t: 6 chars - %u (or PRIu16)\n" + f" int16_t: 7 chars - %d (or PRId16)\n" + f" uint32_t: 11 chars - %" + "PRIu32\n" + " int32_t: 12 chars - %" + "PRId32\n" + " uint64_t: 21 chars - %" + "PRIu64\n" + " int64_t: 21 chars - %" + "PRId64\n" f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n" f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n" f"\n"