mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
Merge branch 'fix/kconfigs_check_move' into 'master'
fix(tools/kconfig): Moved check_konfigs.py to esp-idf-kconfig package Closes IDFGH-9516 See merge request espressif/esp-idf!26033
This commit is contained in:
commit
81804be35f
@ -71,21 +71,6 @@ check_api_usage:
|
|||||||
- tools/ci/check_api_violation.sh
|
- tools/ci/check_api_violation.sh
|
||||||
- tools/ci/check_examples_extra_component_dirs.sh
|
- tools/ci/check_examples_extra_component_dirs.sh
|
||||||
|
|
||||||
test_check_kconfigs:
|
|
||||||
extends: .pre_check_template
|
|
||||||
artifacts:
|
|
||||||
when: on_failure
|
|
||||||
paths:
|
|
||||||
- components/*/Kconfig*.new
|
|
||||||
- examples/*/*/*/Kconfig*.new
|
|
||||||
- examples/*/*/*/*/Kconfig*.new
|
|
||||||
- tools/*/Kconfig*.new
|
|
||||||
- tools/*/*/Kconfig*.new
|
|
||||||
- tools/*/*/*/Kconfig*.new
|
|
||||||
expire_in: 1 week
|
|
||||||
script:
|
|
||||||
- python ${IDF_PATH}/tools/ci/test_check_kconfigs.py
|
|
||||||
|
|
||||||
check_blobs:
|
check_blobs:
|
||||||
extends:
|
extends:
|
||||||
- .pre_check_template
|
- .pre_check_template
|
||||||
|
@ -154,7 +154,6 @@
|
|||||||
- "components/**/*"
|
- "components/**/*"
|
||||||
|
|
||||||
- "tools/ci/test_autocomplete.py"
|
- "tools/ci/test_autocomplete.py"
|
||||||
- "tools/ci/test_check_kconfigs.py"
|
|
||||||
|
|
||||||
- "tools/mass_mfg/**/*"
|
- "tools/mass_mfg/**/*"
|
||||||
|
|
||||||
|
@ -43,7 +43,11 @@ Format rules for Kconfig files are as follows:
|
|||||||
Format Checker
|
Format Checker
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
``tools/ci/check_kconfigs.py`` is provided for checking Kconfig files against the above format rules. The checker checks all Kconfig and ``Kconfig.projbuild`` files in the ESP-IDF directory, and generates a new file with suffix ``.new`` with some suggestions about how to fix issues (if there are any). Please note that the checker cannot correct all format issues and the responsibility of the developer is to final check and make corrections in order to pass the tests. For example, indentations will be corrected if there is not any misleading formatting, but it cannot come up with a common prefix for options inside a menu.
|
``kconfcheck`` tool in esp-idf-kconfig_ package is provided for checking Kconfig files against the above format rules. The checker checks all Kconfig and ``Kconfig.projbuild`` files given as arguments, and generates a new file with suffix ``.new`` with some suggestions about how to fix issues (if there are any). Please note that the checker cannot correct all format issues and the responsibility of the developer is to final check and make corrections in order to pass the tests. For example, indentations will be corrected if there is not any misleading formatting, but it cannot come up with a common prefix for options inside a menu.
|
||||||
|
|
||||||
|
|
||||||
|
The ``esp-idf-kconfig`` package is available in ESP-IDF environments, where the checker tool can be invoked by running command: ``python -m kconfcheck <path_to_kconfig_file>``
|
||||||
|
For more information, see `esp-idf-kconfig package documentation <https://github.com/espressif/esp-idf-kconfig/blob/master/docs/DOCUMENTATION.md>`_
|
||||||
|
|
||||||
.. _configuration-options-compatibility:
|
.. _configuration-options-compatibility:
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
#
|
#
|
||||||
# SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2019-2023 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
@ -11,7 +11,6 @@ import sys
|
|||||||
from io import open
|
from io import open
|
||||||
from typing import Set, Tuple
|
from typing import Set, Tuple
|
||||||
|
|
||||||
from check_kconfigs import valid_directory
|
|
||||||
from idf_ci_utils import get_submodule_dirs
|
from idf_ci_utils import get_submodule_dirs
|
||||||
|
|
||||||
# FILES_TO_CHECK used as "startswith" pattern to match sdkconfig.defaults variants
|
# FILES_TO_CHECK used as "startswith" pattern to match sdkconfig.defaults variants
|
||||||
@ -34,7 +33,7 @@ def _parse_path(path: 'os.PathLike[str]', sep: str=None) -> Set:
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def _valid_directory(path: 'os.PathLike[str]') -> 'os.PathLike[str]':
|
def valid_directory(path: str) -> str:
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
raise argparse.ArgumentTypeError('{} is not a valid directory!'.format(path))
|
raise argparse.ArgumentTypeError('{} is not a valid directory!'.format(path))
|
||||||
return path
|
return path
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
#
|
#
|
||||||
# SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
@ -8,21 +8,18 @@ from __future__ import print_function, unicode_literals
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from io import open
|
|
||||||
|
|
||||||
from idf_ci_utils import IDF_PATH, get_submodule_dirs
|
from idf_ci_utils import IDF_PATH, get_submodule_dirs
|
||||||
|
|
||||||
# regular expression for matching Kconfig files
|
# regular expression for matching Kconfig files
|
||||||
RE_KCONFIG = r'^Kconfig(\.projbuild)?(\.in)?$'
|
RE_KCONFIG = r'^Kconfig(\.projbuild)?(\.in)?$'
|
||||||
|
|
||||||
# ouput file with suggestions will get this suffix
|
|
||||||
OUTPUT_SUFFIX = '.new'
|
|
||||||
|
|
||||||
# ignored directories (makes sense only when run on IDF_PATH)
|
# ignored directories (makes sense only when run on IDF_PATH)
|
||||||
# Note: IGNORE_DIRS is a tuple in order to be able to use it directly with the startswith() built-in function which
|
# Note: ignore_dirs is a tuple in order to be able to use it directly with the startswith() built-in function which
|
||||||
# accepts tuples but no lists.
|
# accepts tuples but no lists.
|
||||||
IGNORE_DIRS = (
|
IGNORE_DIRS: tuple = (
|
||||||
# Kconfigs from submodules need to be ignored:
|
# Kconfigs from submodules need to be ignored:
|
||||||
os.path.join(IDF_PATH, 'components', 'mqtt', 'esp-mqtt'),
|
os.path.join(IDF_PATH, 'components', 'mqtt', 'esp-mqtt'),
|
||||||
# Test Kconfigs are also ignored
|
# Test Kconfigs are also ignored
|
||||||
@ -30,438 +27,62 @@ IGNORE_DIRS = (
|
|||||||
os.path.join(IDF_PATH, 'tools', 'kconfig_new', 'test'),
|
os.path.join(IDF_PATH, 'tools', 'kconfig_new', 'test'),
|
||||||
)
|
)
|
||||||
|
|
||||||
SPACES_PER_INDENT = 4
|
ignore_dirs: tuple = IGNORE_DIRS
|
||||||
|
|
||||||
CONFIG_NAME_MAX_LENGTH = 40
|
|
||||||
|
|
||||||
CONFIG_NAME_MIN_PREFIX_LENGTH = 3
|
|
||||||
|
|
||||||
# The checker will not fail if it encounters this string (it can be used for temporarily resolve conflicts)
|
|
||||||
RE_NOERROR = re.compile(r'\s+#\s+NOERROR\s+$')
|
|
||||||
|
|
||||||
# list or rules for lines
|
|
||||||
LINE_ERROR_RULES = [
|
|
||||||
# (regular expression for finding, error message, correction)
|
|
||||||
(re.compile(r'\t'), 'tabulators should be replaced by spaces', r' ' * SPACES_PER_INDENT),
|
|
||||||
(re.compile(r'\s+\n'), 'trailing whitespaces should be removed', r'\n'),
|
|
||||||
(re.compile(r'.{120}'), 'line should be shorter than 120 characters', None),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class InputError(RuntimeError):
|
def valid_directory(path:str) -> str:
|
||||||
"""
|
|
||||||
Represents and error on the input
|
|
||||||
"""
|
|
||||||
def __init__(self, path, line_number, error_msg, suggested_line):
|
|
||||||
super(InputError, self).__init__('{}:{}: {}'.format(path, line_number, error_msg))
|
|
||||||
self.suggested_line = suggested_line
|
|
||||||
|
|
||||||
|
|
||||||
class BaseChecker(object):
|
|
||||||
"""
|
|
||||||
Base class for all checker objects
|
|
||||||
"""
|
|
||||||
def __init__(self, path_in_idf):
|
|
||||||
self.path_in_idf = path_in_idf
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SourceChecker(BaseChecker):
|
|
||||||
# allow to source only files which will be also checked by the script
|
|
||||||
# Note: The rules are complex and the LineRuleChecker cannot be used
|
|
||||||
def process_line(self, line, line_number):
|
|
||||||
m = re.search(r'^\s*[ro]{0,2}source(\s*)"([^"]+)"', line)
|
|
||||||
|
|
||||||
if m:
|
|
||||||
if len(m.group(1)) == 0:
|
|
||||||
raise InputError(self.path_in_idf, line_number, '"source" has to been followed by space',
|
|
||||||
line.replace('source', 'source '))
|
|
||||||
path = m.group(2)
|
|
||||||
filename = os.path.basename(path)
|
|
||||||
if path in ['$COMPONENT_KCONFIGS_SOURCE_FILE', '$COMPONENT_KCONFIGS_PROJBUILD_SOURCE_FILE']:
|
|
||||||
pass
|
|
||||||
elif not filename.startswith('Kconfig.'):
|
|
||||||
raise InputError(self.path_in_idf, line_number, 'only filenames starting with Kconfig.* can be sourced',
|
|
||||||
line.replace(path, os.path.join(os.path.dirname(path), 'Kconfig.' + filename)))
|
|
||||||
|
|
||||||
|
|
||||||
class LineRuleChecker(BaseChecker):
|
|
||||||
"""
|
|
||||||
checks LINE_ERROR_RULES for each line
|
|
||||||
"""
|
|
||||||
def process_line(self, line, line_number):
|
|
||||||
suppress_errors = RE_NOERROR.search(line) is not None
|
|
||||||
errors = []
|
|
||||||
for rule in LINE_ERROR_RULES:
|
|
||||||
m = rule[0].search(line)
|
|
||||||
if m:
|
|
||||||
if suppress_errors:
|
|
||||||
# just print but no failure
|
|
||||||
e = InputError(self.path_in_idf, line_number, rule[1], line)
|
|
||||||
print(e)
|
|
||||||
else:
|
|
||||||
errors.append(rule[1])
|
|
||||||
if rule[2]:
|
|
||||||
line = rule[0].sub(rule[2], line)
|
|
||||||
if len(errors) > 0:
|
|
||||||
raise InputError(self.path_in_idf, line_number, '; '.join(errors), line)
|
|
||||||
|
|
||||||
|
|
||||||
class IndentAndNameChecker(BaseChecker):
|
|
||||||
"""
|
|
||||||
checks the indentation of each line and configuration names
|
|
||||||
"""
|
|
||||||
def __init__(self, path_in_idf, debug=False):
|
|
||||||
super(IndentAndNameChecker, self).__init__(path_in_idf)
|
|
||||||
self.debug = debug
|
|
||||||
self.min_prefix_length = CONFIG_NAME_MIN_PREFIX_LENGTH
|
|
||||||
|
|
||||||
# stack of the nested menuconfig items, e.g. ['mainmenu', 'menu', 'config']
|
|
||||||
self.level_stack = []
|
|
||||||
|
|
||||||
# stack common prefixes of configs
|
|
||||||
self.prefix_stack = []
|
|
||||||
|
|
||||||
# if the line ends with '\' then we force the indent of the next line
|
|
||||||
self.force_next_indent = 0
|
|
||||||
|
|
||||||
# menu items which increase the indentation of the next line
|
|
||||||
self.re_increase_level = re.compile(r'''^\s*
|
|
||||||
(
|
|
||||||
(menu(?!config))
|
|
||||||
|(mainmenu)
|
|
||||||
|(choice)
|
|
||||||
|(config)
|
|
||||||
|(menuconfig)
|
|
||||||
|(help)
|
|
||||||
|(if)
|
|
||||||
|(source)
|
|
||||||
|(osource)
|
|
||||||
|(rsource)
|
|
||||||
|(orsource)
|
|
||||||
)
|
|
||||||
''', re.X)
|
|
||||||
|
|
||||||
# closing menu items which decrease the indentation
|
|
||||||
self.re_decrease_level = re.compile(r'''^\s*
|
|
||||||
(
|
|
||||||
(endmenu)
|
|
||||||
|(endchoice)
|
|
||||||
|(endif)
|
|
||||||
)
|
|
||||||
''', re.X)
|
|
||||||
|
|
||||||
# matching beginning of the closing menuitems
|
|
||||||
self.pair_dic = {'endmenu': 'menu',
|
|
||||||
'endchoice': 'choice',
|
|
||||||
'endif': 'if',
|
|
||||||
}
|
|
||||||
|
|
||||||
# regex for config names
|
|
||||||
self.re_name = re.compile(r'''^
|
|
||||||
(
|
|
||||||
(?:config)
|
|
||||||
|(?:menuconfig)
|
|
||||||
|(?:choice)
|
|
||||||
|
|
||||||
)\s+
|
|
||||||
(\w+)
|
|
||||||
''', re.X)
|
|
||||||
|
|
||||||
# regex for new prefix stack
|
|
||||||
self.re_new_stack = re.compile(r'''^
|
|
||||||
(
|
|
||||||
(?:menu(?!config))
|
|
||||||
|(?:mainmenu)
|
|
||||||
|(?:choice)
|
|
||||||
|
|
||||||
)
|
|
||||||
''', re.X)
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
super(IndentAndNameChecker, self).__exit__(type, value, traceback)
|
|
||||||
if len(self.prefix_stack) > 0:
|
|
||||||
self.check_common_prefix('', 'EOF')
|
|
||||||
if len(self.prefix_stack) != 0:
|
|
||||||
if self.debug:
|
|
||||||
print(self.prefix_stack)
|
|
||||||
raise RuntimeError("Prefix stack should be empty. Perhaps a menu/choice hasn't been closed")
|
|
||||||
|
|
||||||
def del_from_level_stack(self, count):
|
|
||||||
""" delete count items from the end of the level_stack """
|
|
||||||
if count > 0:
|
|
||||||
# del self.level_stack[-0:] would delete everything and we expect not to delete anything for count=0
|
|
||||||
del self.level_stack[-count:]
|
|
||||||
|
|
||||||
def update_level_for_inc_pattern(self, new_item):
|
|
||||||
if self.debug:
|
|
||||||
print('level+', new_item, ': ', self.level_stack, end=' -> ')
|
|
||||||
# "config" and "menuconfig" don't have a closing pair. So if new_item is an item which need to be indented
|
|
||||||
# outside the last "config" or "menuconfig" then we need to find to a parent where it belongs
|
|
||||||
if new_item in ['config', 'menuconfig', 'menu', 'choice', 'if', 'source', 'rsource', 'osource', 'orsource']:
|
|
||||||
# item is not belonging to a previous "config" or "menuconfig" so need to indent to parent
|
|
||||||
for i, item in enumerate(reversed(self.level_stack)):
|
|
||||||
if item in ['menu', 'mainmenu', 'choice', 'if']:
|
|
||||||
# delete items ("config", "menuconfig", "help") until the appropriate parent
|
|
||||||
self.del_from_level_stack(i)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# delete everything when configs are at top level without a parent menu, mainmenu...
|
|
||||||
self.del_from_level_stack(len(self.level_stack))
|
|
||||||
|
|
||||||
self.level_stack.append(new_item)
|
|
||||||
if self.debug:
|
|
||||||
print(self.level_stack)
|
|
||||||
# The new indent is for the next line. Use the old one for the current line:
|
|
||||||
return len(self.level_stack) - 1
|
|
||||||
|
|
||||||
def update_level_for_dec_pattern(self, new_item):
|
|
||||||
if self.debug:
|
|
||||||
print('level-', new_item, ': ', self.level_stack, end=' -> ')
|
|
||||||
target = self.pair_dic[new_item]
|
|
||||||
for i, item in enumerate(reversed(self.level_stack)):
|
|
||||||
# find the matching beginning for the closing item in reverse-order search
|
|
||||||
# Note: "menuconfig", "config" and "help" don't have closing pairs and they are also on the stack. Now they
|
|
||||||
# will be deleted together with the "menu" or "choice" we are closing.
|
|
||||||
if item == target:
|
|
||||||
i += 1 # delete also the matching beginning
|
|
||||||
if self.debug:
|
|
||||||
print('delete ', i, end=' -> ')
|
|
||||||
self.del_from_level_stack(i)
|
|
||||||
break
|
|
||||||
if self.debug:
|
|
||||||
print(self.level_stack)
|
|
||||||
return len(self.level_stack)
|
|
||||||
|
|
||||||
def check_name_and_update_prefix(self, line, line_number):
|
|
||||||
m = self.re_name.search(line)
|
|
||||||
if m:
|
|
||||||
name = m.group(2)
|
|
||||||
name_length = len(name)
|
|
||||||
|
|
||||||
if name_length > CONFIG_NAME_MAX_LENGTH:
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'{} is {} characters long and it should be {} at most'
|
|
||||||
''.format(name, name_length, CONFIG_NAME_MAX_LENGTH),
|
|
||||||
line + '\n') # no suggested correction for this
|
|
||||||
if len(self.prefix_stack) == 0:
|
|
||||||
self.prefix_stack.append(name)
|
|
||||||
elif self.prefix_stack[-1] is None:
|
|
||||||
self.prefix_stack[-1] = name
|
|
||||||
else:
|
|
||||||
# this has nothing common with paths but the algorithm can be used for this also
|
|
||||||
self.prefix_stack[-1] = os.path.commonprefix([self.prefix_stack[-1], name])
|
|
||||||
if self.debug:
|
|
||||||
print('prefix+', self.prefix_stack)
|
|
||||||
m = self.re_new_stack.search(line)
|
|
||||||
if m:
|
|
||||||
self.prefix_stack.append(None)
|
|
||||||
if self.debug:
|
|
||||||
print('prefix+', self.prefix_stack)
|
|
||||||
|
|
||||||
def check_common_prefix(self, line, line_number):
|
|
||||||
common_prefix = self.prefix_stack.pop()
|
|
||||||
if self.debug:
|
|
||||||
print('prefix-', self.prefix_stack)
|
|
||||||
if common_prefix is None:
|
|
||||||
return
|
|
||||||
common_prefix_len = len(common_prefix)
|
|
||||||
if common_prefix_len < self.min_prefix_length:
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'The common prefix for the config names of the menu ending at this line is "{}".\n'
|
|
||||||
'\tAll config names in this menu should start with the same prefix of {} characters '
|
|
||||||
'or more.'.format(common_prefix, self.min_prefix_length),
|
|
||||||
line) # no suggested correction for this
|
|
||||||
if len(self.prefix_stack) > 0:
|
|
||||||
parent_prefix = self.prefix_stack[-1]
|
|
||||||
if parent_prefix is None:
|
|
||||||
# propagate to parent level where it will influence the prefix checking with the rest which might
|
|
||||||
# follow later on that level
|
|
||||||
self.prefix_stack[-1] = common_prefix
|
|
||||||
else:
|
|
||||||
if len(self.level_stack) > 0 and self.level_stack[-1] in ['mainmenu', 'menu']:
|
|
||||||
# the prefix from menu is not required to propagate to the children
|
|
||||||
return
|
|
||||||
if not common_prefix.startswith(parent_prefix):
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'Common prefix "{}" should start with {}'
|
|
||||||
''.format(common_prefix, parent_prefix),
|
|
||||||
line) # no suggested correction for this
|
|
||||||
|
|
||||||
def process_line(self, line, line_number):
|
|
||||||
stripped_line = line.strip()
|
|
||||||
if len(stripped_line) == 0:
|
|
||||||
self.force_next_indent = 0
|
|
||||||
return
|
|
||||||
current_level = len(self.level_stack)
|
|
||||||
m = re.search(r'\S', line) # indent found as the first non-space character
|
|
||||||
if m:
|
|
||||||
current_indent = m.start()
|
|
||||||
else:
|
|
||||||
current_indent = 0
|
|
||||||
|
|
||||||
if current_level > 0 and self.level_stack[-1] == 'help':
|
|
||||||
if current_indent >= current_level * SPACES_PER_INDENT:
|
|
||||||
# this line belongs to 'help'
|
|
||||||
self.force_next_indent = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.force_next_indent > 0:
|
|
||||||
if current_indent != self.force_next_indent:
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'Indentation consists of {} spaces instead of {}'.format(current_indent,
|
|
||||||
self.force_next_indent),
|
|
||||||
(' ' * self.force_next_indent) + line.lstrip())
|
|
||||||
else:
|
|
||||||
if not stripped_line.endswith('\\'):
|
|
||||||
self.force_next_indent = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
elif stripped_line.endswith('\\') and stripped_line.startswith(('config', 'menuconfig', 'choice')):
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'Line-wrap with backslash is not supported here',
|
|
||||||
line) # no suggestion for this
|
|
||||||
|
|
||||||
self.check_name_and_update_prefix(stripped_line, line_number)
|
|
||||||
|
|
||||||
m = self.re_increase_level.search(line)
|
|
||||||
if m:
|
|
||||||
current_level = self.update_level_for_inc_pattern(m.group(1))
|
|
||||||
else:
|
|
||||||
m = self.re_decrease_level.search(line)
|
|
||||||
if m:
|
|
||||||
new_item = m.group(1)
|
|
||||||
current_level = self.update_level_for_dec_pattern(new_item)
|
|
||||||
if new_item not in ['endif']:
|
|
||||||
# endif doesn't require to check the prefix because the items inside if/endif belong to the
|
|
||||||
# same prefix level
|
|
||||||
self.check_common_prefix(line, line_number)
|
|
||||||
|
|
||||||
expected_indent = current_level * SPACES_PER_INDENT
|
|
||||||
|
|
||||||
if stripped_line.endswith('\\'):
|
|
||||||
self.force_next_indent = expected_indent + SPACES_PER_INDENT
|
|
||||||
else:
|
|
||||||
self.force_next_indent = 0
|
|
||||||
|
|
||||||
if current_indent != expected_indent:
|
|
||||||
raise InputError(self.path_in_idf, line_number,
|
|
||||||
'Indentation consists of {} spaces instead of {}'.format(current_indent, expected_indent),
|
|
||||||
(' ' * expected_indent) + line.lstrip())
|
|
||||||
|
|
||||||
|
|
||||||
def valid_directory(path):
|
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
raise argparse.ArgumentTypeError('{} is not a valid directory!'.format(path))
|
raise argparse.ArgumentTypeError('{} is not a valid directory!'.format(path))
|
||||||
return path
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
def validate_kconfig_file(kconfig_full_path, verbose=False): # type: (str, bool) -> bool
|
parser = argparse.ArgumentParser(description='Kconfig style checker')
|
||||||
suggestions_full_path = kconfig_full_path + OUTPUT_SUFFIX
|
parser.add_argument(
|
||||||
fail = False
|
'files',
|
||||||
|
nargs='*',
|
||||||
|
help='Kconfig files to check (full paths separated by space)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--exclude-submodules',
|
||||||
|
action='store_true',
|
||||||
|
help='Exclude submodules',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--includes',
|
||||||
|
'-d',
|
||||||
|
nargs='*',
|
||||||
|
help='Extra paths for recursively searching Kconfig files. (for example $IDF_PATH)',
|
||||||
|
type=valid_directory
|
||||||
|
)
|
||||||
|
|
||||||
with open(kconfig_full_path, 'r', encoding='utf-8') as f, \
|
args, unknown_args = parser.parse_known_args()
|
||||||
open(suggestions_full_path, 'w', encoding='utf-8', newline='\n') as f_o, \
|
|
||||||
LineRuleChecker(kconfig_full_path) as line_checker, \
|
|
||||||
SourceChecker(kconfig_full_path) as source_checker, \
|
|
||||||
IndentAndNameChecker(kconfig_full_path, debug=verbose) as indent_and_name_checker:
|
|
||||||
try:
|
|
||||||
for line_number, line in enumerate(f, start=1):
|
|
||||||
try:
|
|
||||||
for checker in [line_checker, indent_and_name_checker, source_checker]:
|
|
||||||
checker.process_line(line, line_number)
|
|
||||||
# The line is correct therefore we echo it to the output file
|
|
||||||
f_o.write(line)
|
|
||||||
except InputError as e:
|
|
||||||
print(e)
|
|
||||||
fail = True
|
|
||||||
f_o.write(e.suggested_line)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
raise ValueError('The encoding of {} is not Unicode.'.format(kconfig_full_path))
|
|
||||||
|
|
||||||
if fail:
|
# if the deprecated argument '--exclude-submodules' is used
|
||||||
print('\t{} has been saved with suggestions for resolving the issues.\n'
|
if args.exclude_submodules:
|
||||||
'\tPlease note that the suggestions can be wrong and '
|
ignore_dirs = ignore_dirs + tuple(get_submodule_dirs(full_path=True))
|
||||||
'you might need to re-run the checker several times '
|
|
||||||
'for solving all issues'.format(suggestions_full_path))
|
|
||||||
print('\tPlease fix the errors and run {} for checking the correctness of '
|
|
||||||
'Kconfig files.'.format(os.path.abspath(__file__)))
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print('{}: OK'.format(kconfig_full_path))
|
|
||||||
try:
|
|
||||||
os.remove(suggestions_full_path)
|
|
||||||
except Exception:
|
|
||||||
# not a serious error is when the file cannot be deleted
|
|
||||||
print('{} cannot be deleted!'.format(suggestions_full_path))
|
|
||||||
finally:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
files_to_check: list = []
|
||||||
|
|
||||||
def main():
|
# if the deprecated argument '--includes' is used all valid paths are checked for KConfigs
|
||||||
parser = argparse.ArgumentParser(description='Kconfig style checker')
|
# except IGNORE_DIRS and submodules (if exclude is given)
|
||||||
parser.add_argument('files', nargs='*',
|
# paths to KConfig files are passed to esp-idf-kconfig kconfcheck tool
|
||||||
help='Kconfig files')
|
if args.includes:
|
||||||
parser.add_argument('--verbose', '-v', action='store_true',
|
|
||||||
help='Print more information (useful for debugging)')
|
|
||||||
parser.add_argument('--includes', '-d', nargs='*',
|
|
||||||
help='Extra paths for recursively searching Kconfig files. (for example $IDF_PATH)',
|
|
||||||
type=valid_directory)
|
|
||||||
parser.add_argument('--exclude-submodules', action='store_true',
|
|
||||||
help='Exclude submodules')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
success_counter = 0
|
|
||||||
failure_counter = 0
|
|
||||||
ignore_counter = 0
|
|
||||||
|
|
||||||
ignore_dirs = IGNORE_DIRS
|
|
||||||
if args.exclude_submodules:
|
|
||||||
ignore_dirs = ignore_dirs + tuple(get_submodule_dirs(full_path=True))
|
|
||||||
|
|
||||||
files = [os.path.abspath(file_path) for file_path in args.files]
|
|
||||||
|
|
||||||
if args.includes:
|
|
||||||
for directory in args.includes:
|
for directory in args.includes:
|
||||||
for root, dirnames, filenames in os.walk(directory):
|
for root, dirnames, filenames in os.walk(directory):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
full_path = os.path.join(root, filename)
|
full_path = os.path.join(root, filename)
|
||||||
|
if full_path.startswith(ignore_dirs):
|
||||||
|
continue
|
||||||
if re.search(RE_KCONFIG, filename):
|
if re.search(RE_KCONFIG, filename):
|
||||||
files.append(full_path)
|
files_to_check.append(f'{full_path}')
|
||||||
elif re.search(RE_KCONFIG, filename, re.IGNORECASE):
|
elif re.search(RE_KCONFIG, filename, re.IGNORECASE):
|
||||||
# On Windows Kconfig files are working with different cases!
|
# On Windows Kconfig files are working with different cases!
|
||||||
print('{}: Incorrect filename. The case should be "Kconfig"!'.format(full_path))
|
print(
|
||||||
failure_counter += 1
|
'{}: Incorrect filename. The case should be "Kconfig"!'.format(
|
||||||
|
full_path
|
||||||
for full_path in files:
|
)
|
||||||
if full_path.startswith(ignore_dirs):
|
)
|
||||||
print('{}: Ignored'.format(full_path))
|
|
||||||
ignore_counter += 1
|
|
||||||
continue
|
|
||||||
is_valid = validate_kconfig_file(full_path, args.verbose)
|
|
||||||
if is_valid:
|
|
||||||
success_counter += 1
|
|
||||||
else:
|
|
||||||
failure_counter += 1
|
|
||||||
|
|
||||||
if ignore_counter > 0:
|
|
||||||
print('{} files have been ignored.'.format(ignore_counter))
|
|
||||||
if success_counter > 0:
|
|
||||||
print('{} files have been successfully checked.'.format(success_counter))
|
|
||||||
if failure_counter > 0:
|
|
||||||
print('{} files have errors. Please take a look at the log.'.format(failure_counter))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if not files:
|
|
||||||
print('WARNING: no files specified. Please specify files or use '
|
|
||||||
'"--includes" to search Kconfig files recursively')
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(subprocess.run([sys.executable, '-m', 'kconfcheck'] + files_to_check + unknown_args).returncode)
|
||||||
|
@ -45,7 +45,7 @@ def get_mr_changed_files(source_branch: str) -> t.List[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
git_output = subprocess.check_output(
|
git_output = subprocess.check_output(
|
||||||
['git', 'diff', '--name-only', f'origin/{mr.target_branch}...origin/{source_branch}']
|
['git', 'diff', '--name-only', '--diff-filter=d', f'origin/{mr.target_branch}...origin/{source_branch}']
|
||||||
).decode('utf8')
|
).decode('utf8')
|
||||||
|
|
||||||
return [line.strip() for line in git_output.splitlines() if line.strip()]
|
return [line.strip() for line in git_output.splitlines() if line.strip()]
|
||||||
|
@ -79,7 +79,6 @@ tools/ci/mirror-submodule-update.sh
|
|||||||
tools/ci/multirun_with_pyenv.sh
|
tools/ci/multirun_with_pyenv.sh
|
||||||
tools/ci/push_to_github.sh
|
tools/ci/push_to_github.sh
|
||||||
tools/ci/test_autocomplete.py
|
tools/ci/test_autocomplete.py
|
||||||
tools/ci/test_check_kconfigs.py
|
|
||||||
tools/ci/test_configure_ci_environment.sh
|
tools/ci/test_configure_ci_environment.sh
|
||||||
tools/ci/test_reproducible_build.sh
|
tools/ci/test_reproducible_build.sh
|
||||||
tools/docker/entrypoint.sh
|
tools/docker/entrypoint.sh
|
||||||
|
@ -28,7 +28,6 @@ examples/system/ota/otatool/otatool_example.py
|
|||||||
tools/ble/lib_ble_client.py
|
tools/ble/lib_ble_client.py
|
||||||
tools/ble/lib_gap.py
|
tools/ble/lib_gap.py
|
||||||
tools/ble/lib_gatt.py
|
tools/ble/lib_gatt.py
|
||||||
tools/ci/check_kconfigs.py
|
|
||||||
tools/ci/python_packages/idf_http_server_test/adder.py
|
tools/ci/python_packages/idf_http_server_test/adder.py
|
||||||
tools/ci/python_packages/idf_http_server_test/client.py
|
tools/ci/python_packages/idf_http_server_test/client.py
|
||||||
tools/ci/python_packages/idf_http_server_test/test.py
|
tools/ci/python_packages/idf_http_server_test/test.py
|
||||||
@ -56,7 +55,6 @@ tools/ci/python_packages/ttfw_idf/__init__.py
|
|||||||
tools/ci/python_packages/ttfw_idf/unity_test_parser.py
|
tools/ci/python_packages/ttfw_idf/unity_test_parser.py
|
||||||
tools/ci/python_packages/wifi_tools.py
|
tools/ci/python_packages/wifi_tools.py
|
||||||
tools/ci/test_autocomplete.py
|
tools/ci/test_autocomplete.py
|
||||||
tools/ci/test_check_kconfigs.py
|
|
||||||
tools/esp_app_trace/espytrace/apptrace.py
|
tools/esp_app_trace/espytrace/apptrace.py
|
||||||
tools/esp_app_trace/espytrace/sysview.py
|
tools/esp_app_trace/espytrace/sysview.py
|
||||||
tools/esp_app_trace/logtrace_proc.py
|
tools/esp_app_trace/logtrace_proc.py
|
||||||
|
@ -1,241 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from check_kconfigs import CONFIG_NAME_MAX_LENGTH, IndentAndNameChecker, InputError, LineRuleChecker, SourceChecker
|
|
||||||
|
|
||||||
|
|
||||||
class ApplyLine(object):
|
|
||||||
def apply_line(self, string):
|
|
||||||
self.checker.process_line(string + '\n', 0)
|
|
||||||
|
|
||||||
def expect_error(self, string, expect, cleanup=None):
|
|
||||||
try:
|
|
||||||
with self.assertRaises(InputError) as cm:
|
|
||||||
self.apply_line(string)
|
|
||||||
if expect:
|
|
||||||
self.assertEqual(cm.exception.suggested_line, expect + '\n')
|
|
||||||
finally:
|
|
||||||
if cleanup:
|
|
||||||
# cleanup of the previous failure
|
|
||||||
self.apply_line(cleanup)
|
|
||||||
|
|
||||||
def expt_success(self, string):
|
|
||||||
self.apply_line(string)
|
|
||||||
|
|
||||||
|
|
||||||
class TestLineRuleChecker(unittest.TestCase, ApplyLine):
|
|
||||||
def setUp(self):
|
|
||||||
self.checker = LineRuleChecker('Kconfig')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_tabulators(self):
|
|
||||||
self.expect_error('\ttest', expect=' test')
|
|
||||||
self.expect_error('\t test', expect=' test')
|
|
||||||
self.expect_error(' \ttest', expect=' test')
|
|
||||||
self.expect_error(' \t test', expect=' test')
|
|
||||||
self.expt_success(' test')
|
|
||||||
self.expt_success('test')
|
|
||||||
|
|
||||||
def test_trailing_whitespaces(self):
|
|
||||||
self.expect_error(' ', expect='')
|
|
||||||
self.expect_error(' ', expect='')
|
|
||||||
self.expect_error('test ', expect='test')
|
|
||||||
self.expt_success('test')
|
|
||||||
self.expt_success('')
|
|
||||||
|
|
||||||
def test_line_length(self):
|
|
||||||
self.expect_error('x' * 120, expect=None)
|
|
||||||
self.expt_success('x' * 119)
|
|
||||||
self.expt_success('')
|
|
||||||
|
|
||||||
|
|
||||||
class TestSourceChecker(unittest.TestCase, ApplyLine):
|
|
||||||
def setUp(self):
|
|
||||||
self.checker = SourceChecker('Kconfig')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_source_file_name(self):
|
|
||||||
self.expect_error('source "notKconfig.test"', expect='source "Kconfig.notKconfig.test"')
|
|
||||||
self.expect_error('source "Kconfig"', expect='source "Kconfig.Kconfig"')
|
|
||||||
self.expt_success('source "Kconfig.in"')
|
|
||||||
self.expt_success('source "/tmp/Kconfig.test"')
|
|
||||||
self.expt_success('source "/tmp/Kconfig.in"')
|
|
||||||
self.expect_error('source"Kconfig.in"', expect='source "Kconfig.in"')
|
|
||||||
self.expt_success('source "/tmp/Kconfig.in" # comment')
|
|
||||||
|
|
||||||
|
|
||||||
class TestIndentAndNameChecker(unittest.TestCase, ApplyLine):
|
|
||||||
def setUp(self):
|
|
||||||
self.checker = IndentAndNameChecker('Kconfig')
|
|
||||||
self.checker.min_prefix_length = 4
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.checker.__exit__('Kconfig', None, None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestIndent(TestIndentAndNameChecker):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestIndent, self).setUp()
|
|
||||||
self.checker.min_prefix_length = 0 # prefixes are ignored in this test case
|
|
||||||
|
|
||||||
def test_indent_characters(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expect_error(' test', expect=' test')
|
|
||||||
self.expect_error(' test', expect=' test')
|
|
||||||
self.expect_error(' test', expect=' test')
|
|
||||||
self.expect_error(' test', expect=' test')
|
|
||||||
self.expt_success(' test')
|
|
||||||
self.expt_success(' test2')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expect_error(' default', expect=' default')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expect_error(' text', expect=' text')
|
|
||||||
self.expt_success(' help text')
|
|
||||||
self.expt_success(' menu')
|
|
||||||
self.expt_success(' endmenu')
|
|
||||||
self.expect_error(' choice', expect=' choice', cleanup=' endchoice')
|
|
||||||
self.expect_error(' choice', expect=' choice', cleanup=' endchoice')
|
|
||||||
self.expt_success(' choice')
|
|
||||||
self.expt_success(' endchoice')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
def test_help_content(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expt_success(' description')
|
|
||||||
self.expt_success(' config keyword in the help')
|
|
||||||
self.expt_success(' menu keyword in the help')
|
|
||||||
self.expt_success(' menuconfig keyword in the help')
|
|
||||||
self.expt_success(' endmenu keyword in the help')
|
|
||||||
self.expt_success(' endmenu keyword in the help')
|
|
||||||
self.expt_success('') # newline in help
|
|
||||||
self.expt_success(' endmenu keyword in the help')
|
|
||||||
self.expect_error(' menu "real menu with wrong indent"',
|
|
||||||
expect=' menu "real menu with wrong indent"', cleanup=' endmenu')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
def test_mainmenu(self):
|
|
||||||
self.expt_success('mainmenu "test"')
|
|
||||||
self.expect_error('test', expect=' test')
|
|
||||||
self.expt_success(' not_a_keyword')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success(' menuconfig')
|
|
||||||
self.expect_error('test', expect=' test')
|
|
||||||
self.expect_error(' test', expect=' test')
|
|
||||||
self.expt_success(' menu')
|
|
||||||
self.expt_success(' endmenu')
|
|
||||||
|
|
||||||
def test_ifendif(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expect_error(' if', expect=' if', cleanup=' endif')
|
|
||||||
self.expt_success(' if')
|
|
||||||
self.expect_error(' config', expect=' config')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expt_success(' endif')
|
|
||||||
self.expt_success(' config')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
def test_config_without_menu(self):
|
|
||||||
self.expt_success('menuconfig')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expt_success(' text')
|
|
||||||
self.expt_success('')
|
|
||||||
self.expt_success(' text')
|
|
||||||
self.expt_success('config')
|
|
||||||
self.expt_success(' help')
|
|
||||||
|
|
||||||
def test_source_after_config(self):
|
|
||||||
self.expt_success('menuconfig')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expt_success(' text')
|
|
||||||
self.expect_error(' source', expect='source')
|
|
||||||
self.expt_success('source "Kconfig.in"')
|
|
||||||
|
|
||||||
def test_comment_after_config(self):
|
|
||||||
self.expt_success('menuconfig')
|
|
||||||
self.expt_success(' # comment')
|
|
||||||
self.expt_success(' help')
|
|
||||||
self.expt_success(' text')
|
|
||||||
self.expect_error('# comment', expect=' # comment')
|
|
||||||
self.expt_success(' # second not realcomment"')
|
|
||||||
|
|
||||||
|
|
||||||
class TestName(TestIndentAndNameChecker):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestName, self).setUp()
|
|
||||||
self.checker.min_prefix_length = 0 # prefixes are ignored in this test case
|
|
||||||
|
|
||||||
def test_name_length(self):
|
|
||||||
max_length = CONFIG_NAME_MAX_LENGTH
|
|
||||||
too_long = max_length + 1
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config ABC')
|
|
||||||
self.expt_success(' config ' + ('X' * max_length))
|
|
||||||
self.expect_error(' config ' + ('X' * too_long), expect=None)
|
|
||||||
self.expt_success(' menuconfig ' + ('X' * max_length))
|
|
||||||
self.expect_error(' menuconfig ' + ('X' * too_long), expect=None)
|
|
||||||
self.expt_success(' choice ' + ('X' * max_length))
|
|
||||||
self.expect_error(' choice ' + ('X' * too_long), expect=None)
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrefix(TestIndentAndNameChecker):
|
|
||||||
def test_prefix_len(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config ABC_1')
|
|
||||||
self.expt_success(' config ABC_2')
|
|
||||||
self.expt_success(' config ABC_DEBUG')
|
|
||||||
self.expt_success(' config ABC_ANOTHER')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
self.expt_success('menu "test2"')
|
|
||||||
self.expt_success(' config A')
|
|
||||||
self.expt_success(' config B')
|
|
||||||
self.expect_error('endmenu', expect=None)
|
|
||||||
|
|
||||||
def test_choices(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' choice ASSERTION_LEVEL')
|
|
||||||
self.expt_success(' config ASSERTION_DEBUG')
|
|
||||||
self.expt_success(' config ASSERTION_RELEASE')
|
|
||||||
self.expt_success(' menuconfig ASSERTION_XY')
|
|
||||||
self.expt_success(' endchoice')
|
|
||||||
self.expt_success(' choice DEBUG')
|
|
||||||
self.expt_success(' config DE_1')
|
|
||||||
self.expt_success(' config DE_2')
|
|
||||||
self.expect_error(' endchoice', expect=None)
|
|
||||||
self.expect_error('endmenu', expect=None)
|
|
||||||
|
|
||||||
def test_nested_menu(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config DOESNT_MATTER')
|
|
||||||
self.expt_success(' menu "inner menu"')
|
|
||||||
self.expt_success(' config MENUOP_1')
|
|
||||||
self.expt_success(' config MENUOP_2')
|
|
||||||
self.expt_success(' config MENUOP_3')
|
|
||||||
self.expt_success(' endmenu')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
def test_nested_ifendif(self):
|
|
||||||
self.expt_success('menu "test"')
|
|
||||||
self.expt_success(' config MENUOP_1')
|
|
||||||
self.expt_success(' if MENUOP_1')
|
|
||||||
self.expt_success(' config MENUOP_2')
|
|
||||||
self.expt_success(' endif')
|
|
||||||
self.expt_success('endmenu')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
Loading…
Reference in New Issue
Block a user