mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add additional custom lint checks (#790)
This commit is contained in:
		| @@ -7,6 +7,7 @@ import fnmatch | ||||
| import os.path | ||||
| import subprocess | ||||
| import sys | ||||
| import re | ||||
|  | ||||
|  | ||||
| def find_all(a_str, sub): | ||||
| @@ -39,6 +40,7 @@ ignore_types = ('.ico', '.woff', '.woff2', '') | ||||
|  | ||||
| LINT_FILE_CHECKS = [] | ||||
| LINT_CONTENT_CHECKS = [] | ||||
| LINT_POST_CHECKS = [] | ||||
|  | ||||
|  | ||||
| def run_check(lint_obj, fname, *args): | ||||
| @@ -84,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) | ||||
|  | ||||
| @@ -92,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 | ||||
|  | ||||
| @@ -143,6 +173,98 @@ def lint_end_newline(fname, content): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| 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 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 | ||||
|  | ||||
|  | ||||
| def relative_cpp_search_text(fname, content): | ||||
|     parts = fname.split('/') | ||||
|     integration = parts[2] | ||||
| @@ -241,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