diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a5b1559f2f..e0d4d088a9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -17,9 +17,10 @@ from esphome.const import ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TO CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, \ CONF_TYPE, CONF_PACKAGES from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ - TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes + TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes, DocumentLocation from esphome.helpers import list_starts_with, add_class_to_obj from esphome.voluptuous_schema import _Schema +from esphome.yaml_util import ESPHomeDataBase _LOGGER = logging.getLogger(__name__) @@ -982,7 +983,11 @@ LAMBDA_ENTITY_ID_PROG = re.compile(r'id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\) def lambda_(value): """Coerce this configuration option to a lambda.""" if not isinstance(value, Lambda): - value = Lambda(string_strict(value)) + start_mark = None + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: + start_mark = DocumentLocation.copy(value.esp_range.start_mark) + start_mark.line += value.content_offset + value = Lambda(string_strict(value), start_mark) entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value) if len(entity_id_parts) != 1: entity_ids = ' '.join("'{}'".format(entity_id_parts[i]) diff --git a/esphome/core.py b/esphome/core.py index 0065b750c4..c9f7222a0c 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -227,7 +227,7 @@ LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)') class Lambda: - def __init__(self, value): + def __init__(self, value, start_mark=None): # pylint: disable=protected-access if isinstance(value, Lambda): self._value = value._value @@ -235,6 +235,7 @@ class Lambda: self._value = value self._parts = None self._requires_ids = None + self._source_location = start_mark # https://stackoverflow.com/a/241506/229052 def comment_remover(self, text): @@ -277,6 +278,10 @@ class Lambda: def __repr__(self): return f'Lambda<{self.value}>' + @property + def source_location(self): + return self._source_location + class ID: def __init__(self, id, is_declaration=False, type=None, is_manual=None): @@ -334,9 +339,21 @@ class DocumentLocation: mark.column ) + @classmethod + def copy(cls, location): + return cls( + location.document, + location.line, + location.column + ) + def __str__(self): return f'{self.document} {self.line}:{self.column}' + @property + def as_line_directive(self): + return f'#line {self.line + 1} "{self.document}"' + class DocumentRange: def __init__(self, start_mark: DocumentLocation, end_mark: DocumentLocation): diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 3f87257e26..a82edfd3f7 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,6 +1,7 @@ import abc import inspect import math +import re # pylint: disable=unused-import, wrong-import-order from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence @@ -188,13 +189,14 @@ class ParameterListExpression(Expression): class LambdaExpression(Expression): - __slots__ = ("parts", "parameters", "capture", "return_type") + __slots__ = ("parts", "parameters", "capture", "return_type", "source") - def __init__(self, parts, parameters, capture: str = '=', return_type=None): + def __init__(self, parts, parameters, capture: str = '=', return_type=None, source=None): self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) self.parameters = parameters + self.source = source self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None @@ -202,7 +204,10 @@ class LambdaExpression(Expression): cpp = f'[{self.capture}]({self.parameters})' if self.return_type is not None: cpp += f' -> {self.return_type}' - cpp += f' {{\n{self.content}\n}}' + cpp += ' {\n' + if self.source is not None: + cpp += f'{self.source.as_line_directive}\n' + cpp += f'{self.content}\n}}' return indent_all_but_first_and_last(cpp) @property @@ -360,7 +365,7 @@ class LineComment(Statement): self.value = value def __str__(self): - parts = self.value.split('\n') + parts = re.sub(r'\\\s*\n', r'\n', self.value, re.MULTILINE).split('\n') parts = [f'// {x}' for x in parts] return '\n'.join(parts) @@ -555,7 +560,7 @@ def process_lambda( else: parts[i * 3 + 1] = var parts[i * 3 + 2] = '' - yield LambdaExpression(parts, parameters, capture, return_type) + yield LambdaExpression(parts, parameters, capture, return_type, value.source_location) def is_template(value): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 857a986538..1efe179011 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -11,7 +11,8 @@ import yaml.constructor from esphome import core from esphome.config_helpers import read_config_file -from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange +from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, \ + DocumentRange, DocumentLocation from esphome.helpers import add_class_to_obj from esphome.util import OrderedDict, filter_yaml_files @@ -30,9 +31,16 @@ class ESPHomeDataBase: def esp_range(self): return getattr(self, '_esp_range', None) + @property + def content_offset(self): + return getattr(self, '_content_offset', 0) + def from_node(self, node): # pylint: disable=attribute-defined-outside-init self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) + if isinstance(node, yaml.ScalarNode): + if node.style is not None and node.style in '|>': + self._content_offset = 1 class ESPForceValue: @@ -257,7 +265,10 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_lambda(self, node): - return Lambda(str(node.value)) + start_mark = DocumentLocation.from_mark(node.start_mark) + if node.style is not None and node.style in '|>': + start_mark.line += 1 + return Lambda(str(node.value), start_mark) @_add_data_ref def construct_force(self, node):