mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
feat(ci): add error code registration check to the pre-commit hook
This commit is contained in:
Executable
+146
@@ -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())
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user