2019-12-07 18:28:55 +01:00
|
|
|
#!/usr/bin/env python3
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2021-11-25 21:54:11 +01:00
|
|
|
import argparse
|
2019-04-17 12:06:00 +02:00
|
|
|
import codecs
|
|
|
|
import collections
|
2019-05-11 15:12:30 +02:00
|
|
|
import fnmatch
|
2021-11-25 21:54:11 +01:00
|
|
|
import functools
|
2019-04-17 12:06:00 +02:00
|
|
|
import os.path
|
2019-10-19 22:31:32 +02:00
|
|
|
import re
|
2019-04-17 12:06:00 +02:00
|
|
|
import sys
|
2020-07-29 18:19:48 +02:00
|
|
|
import time
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
import colorama
|
|
|
|
from helpers import filter_changed, git_ls_files, print_error_for_file, styled
|
|
|
|
|
2020-07-29 18:19:48 +02:00
|
|
|
sys.path.append(os.path.dirname(__file__))
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-04-17 12:06:00 +02:00
|
|
|
def find_all(a_str, sub):
|
2020-07-29 18:19:48 +02:00
|
|
|
if not a_str.find(sub):
|
|
|
|
# Optimization: If str is not in whole text, then do not try
|
|
|
|
# on each line
|
|
|
|
return
|
2022-01-26 04:26:46 +01:00
|
|
|
for i, line in enumerate(a_str.split("\n")):
|
2019-04-17 12:06:00 +02:00
|
|
|
column = 0
|
|
|
|
while True:
|
|
|
|
column = line.find(sub, column)
|
|
|
|
if column == -1:
|
|
|
|
break
|
|
|
|
yield i, column
|
|
|
|
column += len(sub)
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
file_types = (
|
|
|
|
".h",
|
|
|
|
".c",
|
|
|
|
".cpp",
|
|
|
|
".tcc",
|
|
|
|
".yaml",
|
|
|
|
".yml",
|
|
|
|
".ini",
|
|
|
|
".txt",
|
|
|
|
".ico",
|
|
|
|
".svg",
|
2023-05-21 22:03:21 +02:00
|
|
|
".png",
|
2021-03-07 20:03:16 +01:00
|
|
|
".py",
|
|
|
|
".html",
|
|
|
|
".js",
|
|
|
|
".md",
|
|
|
|
".sh",
|
|
|
|
".css",
|
|
|
|
".proto",
|
|
|
|
".conf",
|
|
|
|
".cfg",
|
|
|
|
".woff",
|
|
|
|
".woff2",
|
|
|
|
"",
|
|
|
|
)
|
|
|
|
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
2024-04-03 04:16:38 +02:00
|
|
|
py_include = ("*.py",)
|
2024-04-22 00:29:56 +02:00
|
|
|
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf")
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
LINT_FILE_CHECKS = []
|
|
|
|
LINT_CONTENT_CHECKS = []
|
2019-10-23 14:43:27 +02:00
|
|
|
LINT_POST_CHECKS = []
|
2023-12-22 08:03:47 +01:00
|
|
|
EXECUTABLE_BIT = {}
|
|
|
|
|
|
|
|
errors = collections.defaultdict(list)
|
|
|
|
|
|
|
|
|
|
|
|
def add_errors(fname, errs):
|
|
|
|
if not isinstance(errs, list):
|
|
|
|
errs = [errs]
|
|
|
|
for err in errs:
|
|
|
|
if err is None:
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
lineno, col, msg = err
|
|
|
|
except ValueError:
|
|
|
|
lineno = 1
|
|
|
|
col = 1
|
|
|
|
msg = err
|
|
|
|
if not isinstance(msg, str):
|
|
|
|
raise ValueError("Error is not instance of string!")
|
|
|
|
if not isinstance(lineno, int):
|
|
|
|
raise ValueError("Line number is not an int!")
|
|
|
|
if not isinstance(col, int):
|
|
|
|
raise ValueError("Column number is not an int!")
|
|
|
|
errors[fname].append((lineno, col, msg))
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
def run_check(lint_obj, fname, *args):
|
2021-03-07 20:03:16 +01:00
|
|
|
include = lint_obj["include"]
|
|
|
|
exclude = lint_obj["exclude"]
|
|
|
|
func = lint_obj["func"]
|
2019-05-11 15:12:30 +02:00
|
|
|
if include is not None:
|
|
|
|
for incl in include:
|
|
|
|
if fnmatch.fnmatch(fname, incl):
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
for excl in exclude:
|
|
|
|
if fnmatch.fnmatch(fname, excl):
|
|
|
|
return None
|
|
|
|
return func(*args)
|
|
|
|
|
|
|
|
|
|
|
|
def run_checks(lints, fname, *args):
|
|
|
|
for lint in lints:
|
2020-07-29 18:19:48 +02:00
|
|
|
start = time.process_time()
|
|
|
|
try:
|
|
|
|
add_errors(fname, run_check(lint, fname, *args))
|
|
|
|
except Exception:
|
|
|
|
print(f"Check {lint['func'].__name__} on file {fname} failed:")
|
|
|
|
raise
|
|
|
|
duration = time.process_time() - start
|
2021-03-07 20:03:16 +01:00
|
|
|
lint.setdefault("durations", []).append(duration)
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _add_check(checks, func, include=None, exclude=None):
|
2021-03-07 20:03:16 +01:00
|
|
|
checks.append(
|
|
|
|
{
|
|
|
|
"include": include,
|
|
|
|
"exclude": exclude or [],
|
|
|
|
"func": func,
|
|
|
|
}
|
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
def lint_file_check(**kwargs):
|
|
|
|
def decorator(func):
|
|
|
|
_add_check(LINT_FILE_CHECKS, func, **kwargs)
|
|
|
|
return func
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-05-11 15:12:30 +02:00
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
|
|
|
def lint_content_check(**kwargs):
|
|
|
|
def decorator(func):
|
|
|
|
_add_check(LINT_CONTENT_CHECKS, func, **kwargs)
|
|
|
|
return func
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-05-11 15:12:30 +02:00
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_post_check(func):
|
|
|
|
_add_check(LINT_POST_CHECKS, func)
|
|
|
|
return func
|
|
|
|
|
|
|
|
|
|
|
|
def lint_re_check(regex, **kwargs):
|
2021-03-07 20:03:16 +01:00
|
|
|
flags = kwargs.pop("flags", re.MULTILINE)
|
2020-07-14 18:45:42 +02:00
|
|
|
prog = re.compile(regex, flags)
|
2019-10-23 14:43:27 +02:00
|
|
|
decor = lint_content_check(**kwargs)
|
|
|
|
|
|
|
|
def decorator(func):
|
2020-07-29 18:19:48 +02:00
|
|
|
@functools.wraps(func)
|
2019-10-23 14:43:27 +02:00
|
|
|
def new_func(fname, content):
|
2023-12-22 08:03:47 +01:00
|
|
|
errs = []
|
2019-10-23 14:43:27 +02:00
|
|
|
for match in prog.finditer(content):
|
2021-03-07 20:03:16 +01:00
|
|
|
if "NOLINT" in match.group(0):
|
2019-10-23 14:43:27 +02:00
|
|
|
continue
|
|
|
|
lineno = content.count("\n", 0, match.start()) + 1
|
2021-03-07 20:03:16 +01:00
|
|
|
substr = content[: match.start()]
|
|
|
|
col = len(substr) - substr.rfind("\n")
|
2019-10-23 14:43:27 +02:00
|
|
|
err = func(fname, match)
|
|
|
|
if err is None:
|
|
|
|
continue
|
2023-12-22 08:03:47 +01:00
|
|
|
errs.append((lineno, col + 1, err))
|
|
|
|
return errs
|
2020-07-29 18:19:48 +02:00
|
|
|
|
2019-10-23 14:43:27 +02:00
|
|
|
return decor(new_func)
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-10-23 14:43:27 +02:00
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2021-12-01 05:14:25 +01:00
|
|
|
def lint_content_find_check(find, only_first=False, **kwargs):
|
2019-05-11 15:12:30 +02:00
|
|
|
decor = lint_content_check(**kwargs)
|
|
|
|
|
|
|
|
def decorator(func):
|
2020-07-29 18:19:48 +02:00
|
|
|
@functools.wraps(func)
|
2019-05-27 13:06:45 +02:00
|
|
|
def new_func(fname, content):
|
|
|
|
find_ = find
|
|
|
|
if callable(find):
|
|
|
|
find_ = find(fname, content)
|
2023-12-22 08:03:47 +01:00
|
|
|
errs = []
|
2019-05-27 13:06:45 +02:00
|
|
|
for line, col in find_all(content, find_):
|
|
|
|
err = func(fname)
|
2023-12-22 08:03:47 +01:00
|
|
|
errs.append((line + 1, col + 1, err))
|
2021-12-01 05:14:25 +01:00
|
|
|
if only_first:
|
|
|
|
break
|
2023-12-22 08:03:47 +01:00
|
|
|
return errs
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-05-11 15:12:30 +02:00
|
|
|
return decor(new_func)
|
2021-03-07 20:03:16 +01:00
|
|
|
|
2019-05-11 15:12:30 +02:00
|
|
|
return decorator
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_file_check(include=["*.ino"])
|
2019-05-11 15:12:30 +02:00
|
|
|
def lint_ino(fname):
|
|
|
|
return "This file extension (.ino) is not allowed. Please use either .cpp or .h"
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_file_check(
|
|
|
|
exclude=[f"*{f}" for f in file_types]
|
|
|
|
+ [
|
|
|
|
".clang-*",
|
|
|
|
".dockerignore",
|
|
|
|
".editorconfig",
|
|
|
|
"*.gitignore",
|
|
|
|
"LICENSE",
|
|
|
|
"pylintrc",
|
|
|
|
"MANIFEST.in",
|
|
|
|
"docker/Dockerfile*",
|
|
|
|
"docker/rootfs/*",
|
|
|
|
"script/*",
|
|
|
|
]
|
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
def lint_ext_check(fname):
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"This file extension is not a registered file type. If this is an error, please "
|
|
|
|
"update the script/ci-custom.py script."
|
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
2021-09-20 09:07:38 +02:00
|
|
|
@lint_file_check(
|
2022-02-09 11:46:20 +01:00
|
|
|
exclude=[
|
|
|
|
"**.sh",
|
|
|
|
"docker/ha-addon-rootfs/**",
|
|
|
|
"docker/*.py",
|
|
|
|
"script/*",
|
|
|
|
"setup.py",
|
|
|
|
]
|
2021-09-20 09:07:38 +02:00
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
def lint_executable_bit(fname):
|
|
|
|
ex = EXECUTABLE_BIT[fname]
|
|
|
|
if ex != 100644:
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
2023-12-22 08:03:47 +01:00
|
|
|
f"File has invalid executable bit {ex}. If running from a windows machine please "
|
|
|
|
"see disabling executable bit in git."
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_find_check(
|
|
|
|
"\t",
|
2021-12-01 05:14:25 +01:00
|
|
|
only_first=True,
|
2021-03-07 20:03:16 +01:00
|
|
|
exclude=[
|
|
|
|
"esphome/dashboard/static/ace.js",
|
|
|
|
"esphome/dashboard/static/ext-searchbox.js",
|
|
|
|
],
|
|
|
|
)
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_tabs(fname):
|
2019-05-11 15:12:30 +02:00
|
|
|
return "File contains tab character. Please convert tabs to spaces."
|
|
|
|
|
|
|
|
|
2021-12-01 05:14:25 +01:00
|
|
|
@lint_content_find_check("\r", only_first=True)
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_newline(fname):
|
2021-12-01 05:14:25 +01:00
|
|
|
return "File contains Windows newline. Please set your editor to Unix newline mode."
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_check(exclude=["*.svg"])
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_end_newline(fname, content):
|
2021-03-07 20:03:16 +01:00
|
|
|
if content and not content.endswith("\n"):
|
2019-05-11 15:12:30 +02:00
|
|
|
return "File does not end with a newline, please add an empty line at the end of the file."
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2024-04-03 04:16:38 +02:00
|
|
|
CPP_RE_EOL = r".*?(?://.*?)?$"
|
|
|
|
PY_RE_EOL = r".*?(?:#.*?)?$"
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
def highlight(s):
|
2021-03-07 20:03:16 +01:00
|
|
|
return f"\033[36m{s}\033[0m"
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(
|
2024-04-03 04:16:38 +02:00
|
|
|
r"^#define\s+([a-zA-Z0-9_]+)\s+(0b[10]+|0x[0-9a-fA-F]+|\d+)\s*?(?:\/\/.*?)?$",
|
2021-03-07 20:03:16 +01:00
|
|
|
include=cpp_include,
|
2021-11-10 20:55:45 +01:00
|
|
|
exclude=[
|
|
|
|
"esphome/core/log.h",
|
|
|
|
"esphome/components/socket/headers.h",
|
|
|
|
"esphome/core/defines.h",
|
|
|
|
],
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_no_defines(fname, match):
|
2022-02-10 09:55:11 +01:00
|
|
|
s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};")
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"#define macros for integer constants are not allowed, please use "
|
2023-12-22 08:03:47 +01:00
|
|
|
f"{s} style instead (replace uint8_t with the appropriate "
|
|
|
|
"datatype). See also Google style guide."
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(r"^\s*delay\((\d+)\);" + CPP_RE_EOL, include=cpp_include)
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_no_long_delays(fname, match):
|
|
|
|
duration_ms = int(match.group(1))
|
|
|
|
if duration_ms < 50:
|
|
|
|
return None
|
|
|
|
return (
|
2023-12-22 08:03:47 +01:00
|
|
|
f"{highlight(match.group(0).strip())} - 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"
|
2019-10-23 14:43:27 +02:00
|
|
|
"If there's no way to work around the delay() and it doesn't execute often, please add "
|
|
|
|
"a '// NOLINT' comment to the line."
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_check(include=["esphome/const.py"])
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_const_ordered(fname, content):
|
2020-07-29 18:19:48 +02:00
|
|
|
"""Lint that value in const.py are ordered.
|
|
|
|
|
|
|
|
Reason: Otherwise people add it to the end, and then that results in merge conflicts.
|
|
|
|
"""
|
2019-10-23 14:43:27 +02:00
|
|
|
lines = content.splitlines()
|
2023-12-22 08:03:47 +01:00
|
|
|
errs = []
|
2021-03-07 20:03:16 +01:00
|
|
|
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("_", " ")))
|
2019-10-23 14:43:27 +02:00
|
|
|
ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)]
|
2023-12-22 08:03:47 +01:00
|
|
|
for (mi, mline), (_, ol) in zip(matching, ordered):
|
|
|
|
if mline == ol:
|
2019-10-23 14:43:27 +02:00
|
|
|
continue
|
2023-12-22 08:03:47 +01:00
|
|
|
target = next(i for i, line in ordered if line == mline)
|
|
|
|
target_text = next(line for i, line in matching if target == i)
|
|
|
|
errs.append(
|
2021-03-07 20:03:16 +01:00
|
|
|
(
|
|
|
|
mi,
|
|
|
|
1,
|
2023-12-22 08:03:47 +01:00
|
|
|
f"Constant {highlight(mline)} is not ordered, please make sure all "
|
2021-03-07 20:03:16 +01:00
|
|
|
f"constants are ordered. See line {mi} (should go to line {target}, "
|
|
|
|
f"{target_text})",
|
|
|
|
)
|
|
|
|
)
|
2023-12-22 08:03:47 +01:00
|
|
|
return errs
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=["*.py"])
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_conf_matches(fname, match):
|
|
|
|
const = match.group(1)
|
|
|
|
value = match.group(2)
|
|
|
|
const_norm = const.lower()
|
2021-03-07 20:03:16 +01:00
|
|
|
value_norm = value.replace(".", "_")
|
2019-10-23 14:43:27 +02:00
|
|
|
if const_norm == value_norm:
|
|
|
|
return None
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
2023-12-22 08:03:47 +01:00
|
|
|
f"Constant {highlight('CONF_' + const)} does not match value {highlight(value)}! "
|
|
|
|
"Please make sure the constant's name matches its value!"
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
|
2023-12-22 08:03:47 +01:00
|
|
|
with codecs.open("esphome/const.py", "r", encoding="utf-8") as const_f_handle:
|
|
|
|
constants_content = const_f_handle.read()
|
2019-10-23 14:43:27 +02:00
|
|
|
CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)]
|
|
|
|
|
|
|
|
CONSTANTS_USES = collections.defaultdict(list)
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(CONF_RE, include=["*.py"], exclude=["esphome/const.py"])
|
2019-10-23 14:43:27 +02:00
|
|
|
def lint_conf_from_const_py(fname, match):
|
|
|
|
name = match.group(1)
|
|
|
|
if name not in CONSTANTS:
|
|
|
|
CONSTANTS_USES[name].append(fname)
|
|
|
|
return None
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
2023-12-22 08:03:47 +01:00
|
|
|
f"Constant {highlight(name)} has already been defined in const.py - "
|
|
|
|
"please import the constant from const.py directly."
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-23 14:43:27 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
RAW_PIN_ACCESS_RE = (
|
|
|
|
r"^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)"
|
|
|
|
)
|
2020-07-25 14:22:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
@lint_re_check(RAW_PIN_ACCESS_RE, include=cpp_include)
|
|
|
|
def lint_no_raw_pin_access(fname, match):
|
|
|
|
func = match.group(1)
|
|
|
|
pin = match.group(2)
|
|
|
|
mode = match.group(3)
|
|
|
|
new_func = {
|
2021-03-07 20:03:16 +01:00
|
|
|
"pinMode": "pin_mode",
|
|
|
|
"digitalWrite": "digital_write",
|
|
|
|
"digitalRead": "digital_read",
|
2020-07-25 14:22:56 +02:00
|
|
|
}[func]
|
2021-03-07 20:03:16 +01:00
|
|
|
new_code = highlight(f"{pin}->{new_func}({mode})")
|
|
|
|
return f"Don't use raw {func} calls. Instead, use the `->{new_func}` function: {new_code}"
|
2020-07-25 14:22:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
# Functions from Arduino framework that are forbidden to use directly
|
|
|
|
ARDUINO_FORBIDDEN = [
|
2021-03-07 20:03:16 +01:00
|
|
|
"digitalWrite",
|
|
|
|
"digitalRead",
|
|
|
|
"pinMode",
|
|
|
|
"shiftOut",
|
|
|
|
"shiftIn",
|
|
|
|
"radians",
|
|
|
|
"degrees",
|
|
|
|
"interrupts",
|
|
|
|
"noInterrupts",
|
|
|
|
"lowByte",
|
|
|
|
"highByte",
|
|
|
|
"bitRead",
|
|
|
|
"bitSet",
|
|
|
|
"bitClear",
|
|
|
|
"bitWrite",
|
|
|
|
"bit",
|
|
|
|
"analogRead",
|
|
|
|
"analogWrite",
|
|
|
|
"pulseIn",
|
|
|
|
"pulseInLong",
|
|
|
|
"tone",
|
2020-07-25 14:22:56 +02:00
|
|
|
]
|
2021-03-07 20:03:16 +01:00
|
|
|
ARDUINO_FORBIDDEN_RE = r"[^\w\d](" + r"|".join(ARDUINO_FORBIDDEN) + r")\(.*"
|
2020-07-25 14:22:56 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(
|
|
|
|
ARDUINO_FORBIDDEN_RE,
|
|
|
|
include=cpp_include,
|
|
|
|
exclude=[
|
|
|
|
"esphome/components/mqtt/custom_mqtt_device.h",
|
2021-04-07 12:16:36 +02:00
|
|
|
"esphome/components/sun/sun.cpp",
|
2021-03-07 20:03:16 +01:00
|
|
|
],
|
|
|
|
)
|
2020-07-25 14:22:56 +02:00
|
|
|
def lint_no_arduino_framework_functions(fname, match):
|
|
|
|
nolint = highlight("// NOLINT")
|
|
|
|
return (
|
|
|
|
f"The function {highlight(match.group(1))} from the Arduino framework is forbidden to be "
|
|
|
|
f"used directly in the ESPHome codebase. Please use ESPHome's abstractions and equivalent "
|
|
|
|
f"C++ instead.\n"
|
|
|
|
f"\n"
|
|
|
|
f"(If the function is strictly necessary, please add `{nolint}` to the end of the line)"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-09-20 11:47:51 +02:00
|
|
|
IDF_CONVERSION_FORBIDDEN = {
|
|
|
|
"ARDUINO_ARCH_ESP32": "USE_ESP32",
|
|
|
|
"ARDUINO_ARCH_ESP8266": "USE_ESP8266",
|
|
|
|
"pgm_read_byte": "progmem_read_byte",
|
|
|
|
"ICACHE_RAM_ATTR": "IRAM_ATTR",
|
|
|
|
"esphome/core/esphal.h": "esphome/core/hal.h",
|
|
|
|
}
|
|
|
|
IDF_CONVERSION_FORBIDDEN_RE = r"(" + r"|".join(IDF_CONVERSION_FORBIDDEN) + r").*"
|
|
|
|
|
|
|
|
|
|
|
|
@lint_re_check(
|
|
|
|
IDF_CONVERSION_FORBIDDEN_RE,
|
|
|
|
include=cpp_include,
|
|
|
|
)
|
|
|
|
def lint_no_removed_in_idf_conversions(fname, match):
|
|
|
|
replacement = IDF_CONVERSION_FORBIDDEN[match.group(1)]
|
|
|
|
return (
|
|
|
|
f"The macro {highlight(match.group(1))} can no longer be used in ESPHome directly. "
|
2022-06-27 23:02:46 +02:00
|
|
|
f"Please use {highlight(replacement)} instead."
|
2021-09-20 11:47:51 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_re_check(
|
2023-11-28 04:07:29 +01:00
|
|
|
r"[^\w\d]byte +[\w\d]+\s*=",
|
2021-03-07 20:03:16 +01:00
|
|
|
include=cpp_include,
|
|
|
|
exclude={
|
|
|
|
"esphome/components/tuya/tuya.h",
|
|
|
|
},
|
|
|
|
)
|
2020-07-25 14:22:56 +02:00
|
|
|
def lint_no_byte_datatype(fname, match):
|
|
|
|
return (
|
|
|
|
f"The datatype {highlight('byte')} is not allowed to be used in ESPHome. "
|
|
|
|
f"Please use {highlight('uint8_t')} instead."
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-10-23 14:43:27 +02:00
|
|
|
@lint_post_check
|
|
|
|
def lint_constants_usage():
|
2023-12-22 08:03:47 +01:00
|
|
|
errs = []
|
2019-10-23 14:43:27 +02:00
|
|
|
for constant, uses in CONSTANTS_USES.items():
|
2024-05-05 07:32:47 +02:00
|
|
|
if len(uses) < 3:
|
2019-10-23 14:43:27 +02:00
|
|
|
continue
|
2023-12-22 08:03:47 +01:00
|
|
|
errs.append(
|
|
|
|
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
|
|
|
|
f"constant to const.py (Uses: {', '.join(uses)})"
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2023-12-22 08:03:47 +01:00
|
|
|
return errs
|
2019-10-19 22:31:32 +02:00
|
|
|
|
|
|
|
|
2019-05-27 13:06:45 +02:00
|
|
|
def relative_cpp_search_text(fname, content):
|
2021-03-07 20:03:16 +01:00
|
|
|
parts = fname.split("/")
|
2019-05-27 13:06:45 +02:00
|
|
|
integration = parts[2]
|
2019-12-07 18:28:55 +01:00
|
|
|
return f'#include "esphome/components/{integration}'
|
2019-05-27 13:06:45 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_find_check(relative_cpp_search_text, include=["esphome/components/*.cpp"])
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_relative_cpp_import(fname):
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"Component contains absolute import - Components must always use "
|
|
|
|
"relative imports.\n"
|
|
|
|
"Change:\n"
|
|
|
|
' #include "esphome/components/abc/abc.h"\n'
|
|
|
|
"to:\n"
|
|
|
|
' #include "abc.h"\n\n'
|
|
|
|
)
|
2019-05-27 13:06:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
def relative_py_search_text(fname, content):
|
2021-03-07 20:03:16 +01:00
|
|
|
parts = fname.split("/")
|
2019-05-27 13:06:45 +02:00
|
|
|
integration = parts[2]
|
2021-03-07 20:03:16 +01:00
|
|
|
return f"esphome.components.{integration}"
|
2019-05-27 13:06:45 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_find_check(
|
|
|
|
relative_py_search_text,
|
|
|
|
include=["esphome/components/*.py"],
|
2023-09-05 00:16:08 +02:00
|
|
|
exclude=[
|
|
|
|
"esphome/components/libretiny/generate_components.py",
|
|
|
|
"esphome/components/web_server/__init__.py",
|
|
|
|
],
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_relative_py_import(fname):
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"Component contains absolute import - Components must always use "
|
|
|
|
"relative imports within the integration.\n"
|
|
|
|
"Change:\n"
|
|
|
|
' from esphome.components.abc import abc_ns"\n'
|
|
|
|
"to:\n"
|
|
|
|
" from . import abc_ns\n\n"
|
|
|
|
)
|
2019-05-27 13:06:45 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_check(
|
|
|
|
include=[
|
|
|
|
"esphome/components/*.h",
|
|
|
|
"esphome/components/*.cpp",
|
|
|
|
"esphome/components/*.tcc",
|
2021-09-08 05:41:42 +02:00
|
|
|
],
|
|
|
|
exclude=[
|
|
|
|
"esphome/components/socket/headers.h",
|
2021-09-20 11:47:51 +02:00
|
|
|
"esphome/components/esp32/core.cpp",
|
|
|
|
"esphome/components/esp8266/core.cpp",
|
2022-10-20 05:50:39 +02:00
|
|
|
"esphome/components/rp2040/core.cpp",
|
2023-09-05 00:16:08 +02:00
|
|
|
"esphome/components/libretiny/core.cpp",
|
2023-05-10 01:38:18 +02:00
|
|
|
"esphome/components/host/core.cpp",
|
2024-04-24 22:12:32 +02:00
|
|
|
"esphome/components/zephyr/core.cpp",
|
2021-09-08 05:41:42 +02:00
|
|
|
],
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-25 11:32:31 +02:00
|
|
|
def lint_namespace(fname, content):
|
2021-03-07 20:03:16 +01:00
|
|
|
expected_name = re.match(
|
|
|
|
r"^esphome/components/([^/]+)/.*", fname.replace(os.path.sep, "/")
|
|
|
|
).group(1)
|
|
|
|
search = f"namespace {expected_name}"
|
2019-10-25 11:32:31 +02:00
|
|
|
if search in content:
|
|
|
|
return None
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"Invalid namespace found in C++ file. All integration C++ files should put all "
|
|
|
|
"functions in a separate namespace that matches the integration's name. "
|
2023-12-22 08:03:47 +01:00
|
|
|
f"Please make sure the file contains {highlight(search)}"
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2019-10-25 11:32:31 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_find_check('"esphome.h"', include=cpp_include, exclude=["tests/custom.h"])
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_esphome_h(fname):
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"File contains reference to 'esphome.h' - This file is "
|
|
|
|
"auto-generated and should only be used for *custom* "
|
|
|
|
"components. Please replace with references to the direct files."
|
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_check(include=["*.h"])
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_pragma_once(fname, content):
|
2021-03-07 20:03:16 +01:00
|
|
|
if "#pragma once" not in content:
|
|
|
|
return (
|
|
|
|
"Header file contains no 'pragma once' header guard. Please add a "
|
|
|
|
"'#pragma once' line at the top of the file."
|
|
|
|
)
|
2019-05-11 15:12:30 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2020-07-14 18:45:42 +02:00
|
|
|
def lint_inclusive_language(fname, match):
|
|
|
|
# From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"Avoid the use of whitelist/blacklist/slave.\n"
|
|
|
|
"Recommended replacements for 'master / slave' are:\n"
|
|
|
|
" '{primary,main} / {secondary,replica,subordinate}\n"
|
|
|
|
" '{initiator,requester} / {target,responder}'\n"
|
|
|
|
" '{controller,host} / {device,worker,proxy}'\n"
|
|
|
|
" 'leader / follower'\n"
|
|
|
|
" 'director / performer'\n"
|
|
|
|
"\n"
|
|
|
|
"Recommended replacements for 'blacklist/whitelist' are:\n"
|
|
|
|
" 'denylist / allowlist'\n"
|
|
|
|
" 'blocklist / passlist'"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-04-03 04:16:38 +02:00
|
|
|
lint_re_check(
|
|
|
|
r"(whitelist|blacklist|slave)" + PY_RE_EOL,
|
|
|
|
include=py_include,
|
|
|
|
exclude=["script/ci-custom.py"],
|
|
|
|
flags=re.IGNORECASE | re.MULTILINE,
|
|
|
|
)(lint_inclusive_language)
|
|
|
|
|
|
|
|
|
|
|
|
lint_re_check(
|
|
|
|
r"(whitelist|blacklist|slave)" + CPP_RE_EOL,
|
|
|
|
include=cpp_include,
|
|
|
|
flags=re.IGNORECASE | re.MULTILINE,
|
|
|
|
)(lint_inclusive_language)
|
|
|
|
|
|
|
|
|
2022-02-19 14:46:27 +01:00
|
|
|
@lint_re_check(r"[\t\r\f\v ]+$")
|
|
|
|
def lint_trailing_whitespace(fname, match):
|
|
|
|
return "Trailing whitespace detected"
|
|
|
|
|
|
|
|
|
2021-03-07 20:03:16 +01:00
|
|
|
@lint_content_find_check(
|
|
|
|
"ESP_LOG",
|
|
|
|
include=["*.h", "*.tcc"],
|
|
|
|
exclude=[
|
|
|
|
"esphome/components/binary_sensor/binary_sensor.h",
|
2022-01-26 04:26:46 +01:00
|
|
|
"esphome/components/button/button.h",
|
|
|
|
"esphome/components/climate/climate.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/components/cover/cover.h",
|
2024-03-10 19:52:22 +01:00
|
|
|
"esphome/components/datetime/date_entity.h",
|
2024-04-09 03:46:35 +02:00
|
|
|
"esphome/components/datetime/time_entity.h",
|
2024-04-25 23:19:54 +02:00
|
|
|
"esphome/components/datetime/datetime_entity.h",
|
2023-07-05 21:33:26 +02:00
|
|
|
"esphome/components/display/display.h",
|
2024-04-24 04:35:26 +02:00
|
|
|
"esphome/components/event/event.h",
|
2022-01-23 10:21:54 +01:00
|
|
|
"esphome/components/fan/fan.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/components/i2c/i2c.h",
|
2022-02-03 19:24:31 +01:00
|
|
|
"esphome/components/lock/lock.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/components/mqtt/mqtt_component.h",
|
2021-07-12 21:20:12 +02:00
|
|
|
"esphome/components/number/number.h",
|
2023-10-25 11:00:32 +02:00
|
|
|
"esphome/components/text/text.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/components/output/binary_output.h",
|
|
|
|
"esphome/components/output/float_output.h",
|
2021-07-15 02:51:15 +02:00
|
|
|
"esphome/components/nextion/nextion_base.h",
|
2021-08-02 10:00:51 +02:00
|
|
|
"esphome/components/select/select.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/components/sensor/sensor.h",
|
|
|
|
"esphome/components/stepper/stepper.h",
|
|
|
|
"esphome/components/switch/switch.h",
|
|
|
|
"esphome/components/text_sensor/text_sensor.h",
|
2024-04-23 06:47:03 +02:00
|
|
|
"esphome/components/valve/valve.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/core/component.h",
|
2021-09-20 11:47:51 +02:00
|
|
|
"esphome/core/gpio.h",
|
2021-03-07 20:03:16 +01:00
|
|
|
"esphome/core/log.h",
|
|
|
|
"tests/custom.h",
|
|
|
|
],
|
|
|
|
)
|
2019-05-27 13:06:45 +02:00
|
|
|
def lint_log_in_header(fname):
|
2021-03-07 20:03:16 +01:00
|
|
|
return (
|
|
|
|
"Found reference to ESP_LOG in header file. Using ESP_LOG* in header files "
|
|
|
|
"is currently not possible - please move the definition to a source file (.cpp)"
|
|
|
|
)
|
2019-05-12 23:04:36 +02:00
|
|
|
|
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
def main():
|
|
|
|
colorama.init()
|
2019-05-11 15:12:30 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument(
|
|
|
|
"files", nargs="*", default=[], help="files to be processed (regex on path)"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"-c", "--changed", action="store_true", help="Only run on changed files"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--print-slowest", action="store_true", help="Print the slowest checks"
|
|
|
|
)
|
|
|
|
args = parser.parse_args()
|
2019-05-11 15:12:30 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
global EXECUTABLE_BIT
|
|
|
|
EXECUTABLE_BIT = git_ls_files()
|
|
|
|
files = list(EXECUTABLE_BIT.keys())
|
|
|
|
# Match against re
|
|
|
|
file_name_re = re.compile("|".join(args.files))
|
|
|
|
files = [p for p in files if file_name_re.search(p)]
|
|
|
|
|
|
|
|
if args.changed:
|
|
|
|
files = filter_changed(files)
|
|
|
|
|
|
|
|
files.sort()
|
|
|
|
|
|
|
|
for fname in files:
|
|
|
|
_, ext = os.path.splitext(fname)
|
|
|
|
run_checks(LINT_FILE_CHECKS, fname, fname)
|
|
|
|
if ext in ignore_types:
|
2020-07-14 14:34:44 +02:00
|
|
|
continue
|
|
|
|
try:
|
2023-12-22 08:03:47 +01:00
|
|
|
with codecs.open(fname, "r", encoding="utf-8") as f_handle:
|
|
|
|
content = f_handle.read()
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
add_errors(
|
|
|
|
fname,
|
|
|
|
"File is not readable as UTF-8. Please set your editor to UTF-8 mode.",
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
run_checks(LINT_CONTENT_CHECKS, fname, fname, content)
|
2019-05-11 15:12:30 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
run_checks(LINT_POST_CHECKS, "POST")
|
2019-05-11 15:12:30 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
for f, errs in sorted(errors.items()):
|
|
|
|
bold = functools.partial(styled, colorama.Style.BRIGHT)
|
|
|
|
bold_red = functools.partial(styled, (colorama.Style.BRIGHT, colorama.Fore.RED))
|
|
|
|
err_str = (
|
|
|
|
f"{bold(f'{f}:{lineno}:{col}:')} {bold_red('lint:')} {msg}\n"
|
|
|
|
for lineno, col, msg in errs
|
2021-03-07 20:03:16 +01:00
|
|
|
)
|
2023-12-22 08:03:47 +01:00
|
|
|
print_error_for_file(f, "\n".join(err_str))
|
2019-05-11 15:12:30 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
if args.print_slowest:
|
|
|
|
lint_times = []
|
|
|
|
for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS:
|
|
|
|
durations = lint.get("durations", [])
|
|
|
|
lint_times.append((sum(durations), len(durations), lint["func"].__name__))
|
|
|
|
lint_times.sort(key=lambda x: -x[0])
|
|
|
|
for i in range(min(len(lint_times), 10)):
|
|
|
|
dur, invocations, name = lint_times[i]
|
|
|
|
print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)")
|
|
|
|
print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s")
|
2019-10-23 14:43:27 +02:00
|
|
|
|
2023-12-22 08:03:47 +01:00
|
|
|
return len(errors)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
sys.exit(main())
|