mirror of
https://github.com/esphome/esphome.git
synced 2025-06-18 06:15:46 +01:00
Add additional custom lint checks (#790)
This commit is contained in:
esphome
components
binary_sensor_map
coolix
custom
output
dfplayer
esp32_ble_beacon
esp32_ble_tracker
esp32_camera
esp32_touch
mpr121
mqtt
ms5611
ota
pmsx003
restart
rotary_encoder
scd30
sgp30
shutdown
ssd1325_spi
sx1509
binary_sensor
tcl112
template
output
waveshare_epaper
wifi
yashima
script
@ -40,6 +40,7 @@ ignore_types = ('.ico', '.woff', '.woff2', '')
|
||||
|
||||
LINT_FILE_CHECKS = []
|
||||
LINT_CONTENT_CHECKS = []
|
||||
LINT_POST_CHECKS = []
|
||||
|
||||
|
||||
def run_check(lint_obj, fname, *args):
|
||||
@ -85,6 +86,31 @@ def lint_content_check(**kwargs):
|
||||
return decorator
|
||||
|
||||
|
||||
def lint_post_check(func):
|
||||
_add_check(LINT_POST_CHECKS, func)
|
||||
return func
|
||||
|
||||
|
||||
def lint_re_check(regex, **kwargs):
|
||||
prog = re.compile(regex, re.MULTILINE)
|
||||
decor = lint_content_check(**kwargs)
|
||||
|
||||
def decorator(func):
|
||||
def new_func(fname, content):
|
||||
errors = []
|
||||
for match in prog.finditer(content):
|
||||
if 'NOLINT' in match.group(0):
|
||||
continue
|
||||
lineno = content.count("\n", 0, match.start()) + 1
|
||||
err = func(fname, match)
|
||||
if err is None:
|
||||
continue
|
||||
errors.append("{} See line {}.".format(err, lineno))
|
||||
return errors
|
||||
return decor(new_func)
|
||||
return decorator
|
||||
|
||||
|
||||
def lint_content_find_check(find, **kwargs):
|
||||
decor = lint_content_check(**kwargs)
|
||||
|
||||
@ -93,9 +119,12 @@ def lint_content_find_check(find, **kwargs):
|
||||
find_ = find
|
||||
if callable(find):
|
||||
find_ = find(fname, content)
|
||||
errors = []
|
||||
for line, col in find_all(content, find_):
|
||||
err = func(fname)
|
||||
return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1)
|
||||
errors.append("{err} See line {line}:{col}."
|
||||
"".format(err=err, line=line+1, col=col+1))
|
||||
return errors
|
||||
return decor(new_func)
|
||||
return decorator
|
||||
|
||||
@ -144,16 +173,95 @@ def lint_end_newline(fname, content):
|
||||
return None
|
||||
|
||||
|
||||
@lint_content_check(include=['*.cpp', '*.h', '*.tcc'],
|
||||
exclude=['esphome/core/log.h'])
|
||||
def lint_no_defines(fname, content):
|
||||
CPP_RE_EOL = r'\s*?(?://.*?)?$'
|
||||
|
||||
|
||||
def highlight(s):
|
||||
return '\033[36m{}\033[0m'.format(s)
|
||||
|
||||
|
||||
@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL,
|
||||
include=cpp_include, exclude=['esphome/core/log.h'])
|
||||
def lint_no_defines(fname, match):
|
||||
s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2)))
|
||||
return ("#define macros for integer constants are not allowed, please use "
|
||||
"{} style instead (replace uint8_t with the appropriate "
|
||||
"datatype). See also Google style guide.".format(s))
|
||||
|
||||
|
||||
@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include)
|
||||
def lint_no_long_delays(fname, match):
|
||||
duration_ms = int(match.group(1))
|
||||
if duration_ms < 50:
|
||||
return None
|
||||
return (
|
||||
"{} - long calls to delay() are not allowed in ESPHome because everything executes "
|
||||
"in one thread. Calling delay() will block the main thread and slow down ESPHome.\n"
|
||||
"If there's no way to work around the delay() and it doesn't execute often, please add "
|
||||
"a '// NOLINT' comment to the line."
|
||||
"".format(highlight(match.group(0).strip()))
|
||||
)
|
||||
|
||||
|
||||
@lint_content_check(include=['esphome/const.py'])
|
||||
def lint_const_ordered(fname, content):
|
||||
lines = content.splitlines()
|
||||
errors = []
|
||||
for match in re.finditer(r'#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)', content, re.MULTILINE):
|
||||
errors.append(
|
||||
"#define macros for integer constants are not allowed, please use "
|
||||
"`static const uint8_t {} = {};` style instead (replace uint8_t with the appropriate "
|
||||
"datatype). See also Google styleguide.".format(match.group(1), match.group(2))
|
||||
)
|
||||
for start in ['CONF_', 'ICON_', 'UNIT_']:
|
||||
matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)]
|
||||
ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' ')))
|
||||
ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)]
|
||||
for (mi, ml), (oi, ol) in zip(matching, ordered):
|
||||
if ml == ol:
|
||||
continue
|
||||
target = next(i for i, l in ordered if l == ml)
|
||||
target_text = next(l for i, l in matching if target == i)
|
||||
errors.append("Constant {} is not ordered, please make sure all constants are ordered. "
|
||||
"See line {} (should go to line {}, {})"
|
||||
"".format(highlight(ml), mi, target, target_text))
|
||||
return errors
|
||||
|
||||
|
||||
@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py'])
|
||||
def lint_conf_matches(fname, match):
|
||||
const = match.group(1)
|
||||
value = match.group(2)
|
||||
const_norm = const.lower()
|
||||
value_norm = value.replace('.', '_')
|
||||
if const_norm == value_norm:
|
||||
return None
|
||||
return ("Constant {} does not match value {}! Please make sure the constant's name matches its "
|
||||
"value!"
|
||||
"".format(highlight('CONF_' + const), highlight(value)))
|
||||
|
||||
|
||||
CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
|
||||
with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle:
|
||||
constants_content = f_handle.read()
|
||||
CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)]
|
||||
|
||||
CONSTANTS_USES = collections.defaultdict(list)
|
||||
|
||||
|
||||
@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py'])
|
||||
def lint_conf_from_const_py(fname, match):
|
||||
name = match.group(1)
|
||||
if name not in CONSTANTS:
|
||||
CONSTANTS_USES[name].append(fname)
|
||||
return None
|
||||
return ("Constant {} has already been defined in const.py - please import the constant from "
|
||||
"const.py directly.".format(highlight(name)))
|
||||
|
||||
|
||||
@lint_post_check
|
||||
def lint_constants_usage():
|
||||
errors = []
|
||||
for constant, uses in CONSTANTS_USES.items():
|
||||
if len(uses) < 4:
|
||||
continue
|
||||
errors.append("Constant {} is defined in {} files. Please move all definitions of the "
|
||||
"constant to const.py (Uses: {})"
|
||||
"".format(highlight(constant), len(uses), ', '.join(uses)))
|
||||
return errors
|
||||
|
||||
|
||||
@ -255,6 +363,8 @@ for fname in files:
|
||||
continue
|
||||
run_checks(LINT_CONTENT_CHECKS, fname, fname, content)
|
||||
|
||||
run_checks(LINT_POST_CHECKS, 'POST')
|
||||
|
||||
for f, errs in sorted(errors.items()):
|
||||
print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f))
|
||||
for err in errs:
|
||||
|
Reference in New Issue
Block a user