diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 274bb18186..d8a9eaccc3 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -616,6 +616,12 @@ def _wrap_in_iifes(lines: list[str], max_statements: int | None) -> list[str]: out: list[str] = [] chunk: list[str] = [] depth: int = 0 + # Once depth goes negative we stop trusting the brace count and + # keep everything remaining in one final IIFE. A later ``{`` could + # arithmetically bring depth back to 0, but by that point the brace + # tracking is already unreliable — re-enabling mid-stream splits + # could land between a declaration and its use. + poisoned: bool = False def flush() -> None: if not chunk: @@ -631,11 +637,16 @@ def _wrap_in_iifes(lines: list[str], max_statements: int | None) -> list[str]: for line in lines: chunk.append(line) # Count { and } per line so inline control flow (e.g. `if (cond) {`) - # and balanced inline lambdas are tracked correctly. If depth ever - # goes negative (unbalanced input) we never return to 0 and the - # rest falls through into a single final IIFE — safe fallback. + # and balanced inline lambdas are tracked correctly. depth += line.count("{") - line.count("}") - if max_statements is not None and depth == 0 and len(chunk) >= max_statements: + if depth < 0: + poisoned = True + if ( + not poisoned + and max_statements is not None + and depth == 0 + and len(chunk) >= max_statements + ): flush() flush() return out diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index a4e1d0b8bd..ad84c12d8e 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -943,6 +943,20 @@ def test_wrap_in_iifes_unbalanced_braces_fall_through() -> None: assert [line for line in result if line in lines] == lines +def test_wrap_in_iifes_negative_depth_stays_poisoned() -> None: + # Once depth goes negative the brace tracker is unreliable; a later + # arithmetic return to depth 0 must not re-enable splitting. Here + # "}" drives depth to -1 immediately, then "{" later brings depth + # back to 0 arithmetically. With max=1 every statement would flush + # if splitting were still enabled — assert everything emits as one + # IIFE because the poisoned flag stayed set. + lines = ["}", "a();", "{", "b();", "}", "c();"] + result = core._wrap_in_iifes(lines, max_statements=1) + assert sum(1 for line in result if line == "[]() {") == 1 + assert result[0] == "[]() {" + assert result[-1] == "}();" + + def test_wrap_in_iifes_never_splits_inline_brace_lines() -> None: # Defensive: if codegen ever emits control flow with braces on the # same line (if/else/for), the depth tracker should keep the whole