From 23b8a17d3fe240d01a62b9cdeadfe620759b0c41 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Wed, 8 Apr 2026 07:59:40 +0200 Subject: [PATCH] feat(ci): add error code registration check to the pre-commit hook --- .pre-commit-config.yaml | 6 + tools/ci/check_err_codes_registration.py | 146 +++++++++++++++++++++++ tools/ci/executable-list.txt | 1 + 3 files changed, 153 insertions(+) create mode 100755 tools/ci/check_err_codes_registration.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cce9693e0a..bffb0293bb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -189,6 +189,12 @@ repos: )$ pass_filenames: false verbose: true + - id: check-err-codes-registration + name: Check error code headers are registered + entry: tools/ci/check_err_codes_registration.py + language: python + files: 'components/.+\.h$|components/.+CMakeLists\.txt$' + pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: diff --git a/tools/ci/check_err_codes_registration.py b/tools/ci/check_err_codes_registration.py new file mode 100755 index 00000000000..3607ce901f7 --- /dev/null +++ b/tools/ci/check_err_codes_registration.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +""" +Pre-commit check: ensure every header defining ESP_ERR_* codes is registered +via idf_define_esp_err_codes() in its component's CMakeLists.txt. + +Compares the set of headers found by scanning all components against those +explicitly listed in idf_define_esp_err_codes(HEADERS ...) calls. +""" + +import os +import re +import sys + +# Add tools/ to path so we can import err_codes_extract +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from err_codes_extract import EXCLUDE_SEARCH_DIRS +from err_codes_extract import extract_from_file + +# Additional directories to exclude beyond those in err_codes_extract +EXTRA_EXCLUDE_DIRS = {'remote'} + +# Pattern matching idf_define_esp_err_codes(HEADERS ...) in CMakeLists.txt +ERR_CODES_CALL = re.compile(r'idf_define_esp_err_codes\s*\(') + + +def find_all_err_headers(components_dir: str) -> dict[str, set[str]]: + """ + Scan all component headers for ESP_ERR_* definitions using + err_codes_extract.extract_from_file(). + + Returns: + dict mapping component name -> set of header paths (relative to component dir) + """ + exclude = EXCLUDE_SEARCH_DIRS | EXTRA_EXCLUDE_DIRS + result: dict[str, set[str]] = {} + + for component_name in sorted(os.listdir(components_dir)): + component_dir = os.path.join(components_dir, component_name) + if not os.path.isdir(component_dir): + continue + + for root, dirnames, filenames in os.walk(component_dir, topdown=True): + dirnames[:] = [d for d in dirnames if d not in exclude] + for filename in filenames: + if not filename.endswith('.h'): + continue + filepath = os.path.join(root, filename) + rel_path = os.path.relpath(filepath, component_dir) + if extract_from_file(filepath, rel_path): + result.setdefault(component_name, set()).add(rel_path) + + return result + + +def parse_registered_headers(components_dir: str) -> dict[str, set[str]]: + """ + Parse all CMakeLists.txt files for idf_define_esp_err_codes(HEADERS ...) calls. + + Returns: + dict mapping component name -> set of header paths (relative to component dir) + """ + result: dict[str, set[str]] = {} + + for component_name in sorted(os.listdir(components_dir)): + component_dir = os.path.join(components_dir, component_name) + cmake_file = os.path.join(component_dir, 'CMakeLists.txt') + if not os.path.isfile(cmake_file): + continue + + try: + with open(cmake_file, encoding='utf-8') as f: + content = f.read() + except (UnicodeDecodeError, OSError): + continue + + # Find all idf_define_esp_err_codes() calls and extract HEADERS args + for match in ERR_CODES_CALL.finditer(content): + start = match.end() + # Find the matching closing paren, handling nesting + depth = 1 + pos = start + while pos < len(content) and depth > 0: + if content[pos] == '(': + depth += 1 + elif content[pos] == ')': + depth -= 1 + pos += 1 + + args_str = content[start : pos - 1] + # Remove comments + args_str = re.sub(r'#.*$', '', args_str, flags=re.MULTILINE) + # Extract tokens after HEADERS keyword + headers_match = re.search(r'\bHEADERS\b\s+(.*)', args_str, re.DOTALL) + if headers_match: + tokens = headers_match.group(1).split() + # Stop at next keyword (uppercase word) or end + for token in tokens: + if re.match(r'^[A-Z_]+$', token): + break + # Normalize path separators + header_path = token.replace('\\', '/') + result.setdefault(component_name, set()).add(header_path) + + return result + + +def main() -> int: + idf_path = os.environ.get('IDF_PATH', os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + components_dir = os.path.join(idf_path, 'components') + + if not os.path.isdir(components_dir): + print(f'Error: components directory not found: {components_dir}', file=sys.stderr) + return 1 + + found_headers = find_all_err_headers(components_dir) + registered_headers = parse_registered_headers(components_dir) + + errors: list[str] = [] + + for component, headers in sorted(found_headers.items()): + registered = registered_headers.get(component, set()) + missing = headers - registered + if missing: + for header in sorted(missing): + errors.append( + f'{component}: {header} defines ESP_ERR_* codes but is not registered in idf_define_esp_err_codes()' + ) + + if errors: + print('Error code registration check failed:', file=sys.stderr) + for error in errors: + print(f' {error}', file=sys.stderr) + print( + f'\n{len(errors)} header(s) define ESP_ERR_* codes but are not registered.\n' + f"Add idf_define_esp_err_codes(HEADERS ...) to the component's CMakeLists.txt.", + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/ci/executable-list.txt b/tools/ci/executable-list.txt index 45b9d810dc5..e0097ed5cc0 100644 --- a/tools/ci/executable-list.txt +++ b/tools/ci/executable-list.txt @@ -57,6 +57,7 @@ tools/ci/check_api_violation.sh tools/ci/check_build_test_rules.py tools/ci/check_callgraph.py tools/ci/check_codeowners.py +tools/ci/check_err_codes_registration.py tools/ci/check_esp_memory_utils_headers.sh tools/ci/check_examples_extra_component_dirs.sh tools/ci/check_executables.py