|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # Copyright (c) 2019 Nordic Semiconductor ASA | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  |  | 
|  | """ | 
|  | Linter for the Zephyr Kconfig files. Pass --help to see | 
|  | available checks. By default, all checks are enabled. | 
|  |  | 
|  | Some of the checks rely on heuristics and can get tripped up | 
|  | by things like preprocessor magic, so manual checking is | 
|  | still needed. 'git grep' is handy. | 
|  |  | 
|  | Requires west, because the checks need to see Kconfig files | 
|  | and source code from modules. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import re | 
|  | import shlex | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  |  | 
|  | TOP_DIR = os.path.join(os.path.dirname(__file__), "..", "..") | 
|  |  | 
|  | sys.path.insert(0, os.path.join(TOP_DIR, "scripts", "kconfig")) | 
|  | import kconfiglib | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | init_kconfig() | 
|  |  | 
|  | args = parse_args() | 
|  | if args.checks: | 
|  | checks = args.checks | 
|  | else: | 
|  | # Run all checks if no checks were specified | 
|  | checks = (check_always_n, | 
|  | check_unused, | 
|  | check_pointless_menuconfigs, | 
|  | check_defconfig_only_definition, | 
|  | check_missing_config_prefix) | 
|  |  | 
|  | first = True | 
|  | for check in checks: | 
|  | if not first: | 
|  | print() | 
|  | first = False | 
|  | check() | 
|  |  | 
|  |  | 
|  | def parse_args(): | 
|  | # args.checks is set to a list of check functions to run | 
|  |  | 
|  | parser = argparse.ArgumentParser( | 
|  | formatter_class=argparse.RawTextHelpFormatter, | 
|  | description=__doc__, allow_abbrev=False) | 
|  |  | 
|  | parser.add_argument( | 
|  | "-n", "--check-always-n", | 
|  | action="append_const", dest="checks", const=check_always_n, | 
|  | help="""\ | 
|  | List symbols that can never be anything but n/empty. These | 
|  | are detected as symbols with no prompt or defaults that | 
|  | aren't selected or implied. | 
|  | """) | 
|  |  | 
|  | parser.add_argument( | 
|  | "-u", "--check-unused", | 
|  | action="append_const", dest="checks", const=check_unused, | 
|  | help="""\ | 
|  | List symbols that might be unused. | 
|  |  | 
|  | Heuristic: | 
|  |  | 
|  | - Isn't referenced in Kconfig | 
|  | - Isn't referenced as CONFIG_<NAME> outside Kconfig | 
|  | (besides possibly as CONFIG_<NAME>=<VALUE>) | 
|  | - Isn't selecting/implying other symbols | 
|  | - Isn't a choice symbol | 
|  |  | 
|  | C preprocessor magic can trip up this check.""") | 
|  |  | 
|  | parser.add_argument( | 
|  | "-m", "--check-pointless-menuconfigs", | 
|  | action="append_const", dest="checks", const=check_pointless_menuconfigs, | 
|  | help="""\ | 
|  | List symbols defined with 'menuconfig' where the menu is | 
|  | empty due to the symbol not being followed by stuff that | 
|  | depends on it""") | 
|  |  | 
|  | parser.add_argument( | 
|  | "-d", "--check-defconfig-only-definition", | 
|  | action="append_const", dest="checks", const=check_defconfig_only_definition, | 
|  | help="""\ | 
|  | List symbols that are only defined in Kconfig.defconfig | 
|  | files. A common base definition should probably be added | 
|  | somewhere for such symbols, and the type declaration ('int', | 
|  | 'hex', etc.) removed from Kconfig.defconfig.""") | 
|  |  | 
|  | parser.add_argument( | 
|  | "-p", "--check-missing-config-prefix", | 
|  | action="append_const", dest="checks", const=check_missing_config_prefix, | 
|  | help="""\ | 
|  | Look for references like | 
|  |  | 
|  | #if MACRO | 
|  | #if(n)def MACRO | 
|  | defined(MACRO) | 
|  | IS_ENABLED(MACRO) | 
|  |  | 
|  | where MACRO is the name of a defined Kconfig symbol but | 
|  | doesn't have a CONFIG_ prefix. Could be a typo. | 
|  |  | 
|  | Macros that are #define'd somewhere are not flagged.""") | 
|  |  | 
|  | return parser.parse_args() | 
|  |  | 
|  |  | 
|  | def check_always_n(): | 
|  | print_header("Symbols that can't be anything but n/empty") | 
|  | for sym in kconf.unique_defined_syms: | 
|  | if not has_prompt(sym) and not is_selected_or_implied(sym) and \ | 
|  | not has_defaults(sym): | 
|  | print(name_and_locs(sym)) | 
|  |  | 
|  |  | 
|  | def check_unused(): | 
|  | print_header("Symbols that look unused") | 
|  | referenced = referenced_sym_names() | 
|  | for sym in kconf.unique_defined_syms: | 
|  | if not is_selecting_or_implying(sym) and not sym.choice and \ | 
|  | sym.name not in referenced: | 
|  | print(name_and_locs(sym)) | 
|  |  | 
|  |  | 
|  | def check_pointless_menuconfigs(): | 
|  | print_header("menuconfig symbols with empty menus") | 
|  | for node in kconf.node_iter(): | 
|  | if node.is_menuconfig and not node.list and \ | 
|  | isinstance(node.item, kconfiglib.Symbol): | 
|  | print("{0.item.name:40} {0.filename}:{0.linenr}".format(node)) | 
|  |  | 
|  |  | 
|  | def check_defconfig_only_definition(): | 
|  | print_header("Symbols only defined in Kconfig.defconfig files") | 
|  | for sym in kconf.unique_defined_syms: | 
|  | if all("defconfig" in node.filename for node in sym.nodes): | 
|  | print(name_and_locs(sym)) | 
|  |  | 
|  |  | 
|  | def check_missing_config_prefix(): | 
|  | print_header("Symbol references that might be missing a CONFIG_ prefix") | 
|  |  | 
|  | # Paths to modules | 
|  | modpaths = run(("west", "list", "-f{abspath}")).splitlines() | 
|  |  | 
|  | # Gather #define'd macros that might overlap with symbol names, so that | 
|  | # they don't trigger false positives | 
|  | defined = set() | 
|  | for modpath in modpaths: | 
|  | regex = r"#\s*define\s+([A-Z0-9_]+)\b" | 
|  | defines = run(("git", "grep", "--extended-regexp", regex), | 
|  | cwd=modpath, check=False) | 
|  | # Could pass --only-matching to git grep as well, but it was added | 
|  | # pretty recently (2018) | 
|  | defined.update(re.findall(regex, defines)) | 
|  |  | 
|  | # Filter out symbols whose names are #define'd too. Preserve definition | 
|  | # order to make the output consistent. | 
|  | syms = [sym for sym in kconf.unique_defined_syms | 
|  | if sym.name not in defined] | 
|  |  | 
|  | # grep for symbol references in #ifdef/defined() that are missing a CONFIG_ | 
|  | # prefix. Work around an "argument list too long" error from 'git grep' by | 
|  | # checking symbols in batches. | 
|  | for batch in split_list(syms, 200): | 
|  | # grep for '#if((n)def) <symbol>', 'defined(<symbol>', and | 
|  | # 'IS_ENABLED(<symbol>', with a missing CONFIG_ prefix | 
|  | regex = r"(?:#\s*if(?:n?def)\s+|\bdefined\s*\(\s*|IS_ENABLED\(\s*)(?:" + \ | 
|  | "|".join(sym.name for sym in batch) + r")\b" | 
|  | cmd = ("git", "grep", "--line-number", "-I", "--perl-regexp", regex) | 
|  |  | 
|  | for modpath in modpaths: | 
|  | print(run(cmd, cwd=modpath, check=False), end="") | 
|  |  | 
|  |  | 
|  | def split_list(lst, batch_size): | 
|  | # check_missing_config_prefix() helper generator that splits a list into | 
|  | # equal-sized batches (possibly with a shorter batch at the end) | 
|  |  | 
|  | for i in range(0, len(lst), batch_size): | 
|  | yield lst[i:i + batch_size] | 
|  |  | 
|  |  | 
|  | def print_header(s): | 
|  | print(s + "\n" + len(s)*"=") | 
|  |  | 
|  |  | 
|  | def init_kconfig(): | 
|  | global kconf | 
|  |  | 
|  | os.environ.update( | 
|  | srctree=TOP_DIR, | 
|  | CMAKE_BINARY_DIR=modules_file_dir(), | 
|  | KCONFIG_DOC_MODE="1", | 
|  | ZEPHYR_BASE=TOP_DIR, | 
|  | SOC_DIR="soc", | 
|  | ARCH_DIR="arch", | 
|  | KCONFIG_BOARD_DIR="boards/*/*", | 
|  | ARCH="*") | 
|  |  | 
|  | kconf = kconfiglib.Kconfig(suppress_traceback=True) | 
|  |  | 
|  |  | 
|  | def modules_file_dir(): | 
|  | # Creates Kconfig.modules in a temporary directory and returns the path to | 
|  | # the directory. Kconfig.modules brings in Kconfig files from modules. | 
|  |  | 
|  | tmpdir = tempfile.mkdtemp() | 
|  | run((os.path.join("scripts", "zephyr_module.py"), | 
|  | "--kconfig-out", os.path.join(tmpdir, "Kconfig.modules"))) | 
|  | return tmpdir | 
|  |  | 
|  |  | 
|  | def referenced_sym_names(): | 
|  | # Returns the names of all symbols referenced inside and outside the | 
|  | # Kconfig files (that we can detect), without any "CONFIG_" prefix | 
|  |  | 
|  | return referenced_in_kconfig() | referenced_outside_kconfig() | 
|  |  | 
|  |  | 
|  | def referenced_in_kconfig(): | 
|  | # Returns the names of all symbols referenced inside the Kconfig files | 
|  |  | 
|  | return {ref.name | 
|  | for node in kconf.node_iter() | 
|  | for ref in node.referenced | 
|  | if isinstance(ref, kconfiglib.Symbol)} | 
|  |  | 
|  |  | 
|  | def referenced_outside_kconfig(): | 
|  | # Returns the names of all symbols referenced outside the Kconfig files | 
|  |  | 
|  | regex = r"\bCONFIG_[A-Z0-9_]+\b" | 
|  |  | 
|  | res = set() | 
|  |  | 
|  | # 'git grep' all modules | 
|  | for modpath in run(("west", "list", "-f{abspath}")).splitlines(): | 
|  | for line in run(("git", "grep", "-h", "-I", "--extended-regexp", regex), | 
|  | cwd=modpath).splitlines(): | 
|  | # Don't record lines starting with "CONFIG_FOO=" or "# CONFIG_FOO=" | 
|  | # as references, so that symbols that are only assigned in .config | 
|  | # files are not included | 
|  | if re.match(r"[\s#]*CONFIG_[A-Z0-9_]+=.*", line): | 
|  | continue | 
|  |  | 
|  | # Could pass --only-matching to git grep as well, but it was added | 
|  | # pretty recently (2018) | 
|  | for match in re.findall(regex, line): | 
|  | res.add(match[7:])  # Strip "CONFIG_" | 
|  |  | 
|  | return res | 
|  |  | 
|  |  | 
|  | def has_prompt(sym): | 
|  | return any(node.prompt for node in sym.nodes) | 
|  |  | 
|  |  | 
|  | def is_selected_or_implied(sym): | 
|  | return sym.rev_dep is not kconf.n or sym.weak_rev_dep is not kconf.n | 
|  |  | 
|  |  | 
|  | def has_defaults(sym): | 
|  | return bool(sym.defaults) | 
|  |  | 
|  |  | 
|  | def is_selecting_or_implying(sym): | 
|  | return sym.selects or sym.implies | 
|  |  | 
|  |  | 
|  | def name_and_locs(sym): | 
|  | # Returns a string with the name and definition location(s) for 'sym' | 
|  |  | 
|  | return "{:40} {}".format( | 
|  | sym.name, | 
|  | ", ".join("{0.filename}:{0.linenr}".format(node) for node in sym.nodes)) | 
|  |  | 
|  |  | 
|  | def run(cmd, cwd=TOP_DIR, check=True): | 
|  | # Runs 'cmd' with subprocess, returning the decoded stdout output. 'cwd' is | 
|  | # the working directory. It defaults to the top-level Zephyr directory. | 
|  | # Exits with an error if the command exits with a non-zero return code if | 
|  | # 'check' is True. | 
|  |  | 
|  | cmd_s = " ".join(shlex.quote(word) for word in cmd) | 
|  |  | 
|  | try: | 
|  | process = subprocess.Popen( | 
|  | cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) | 
|  | except OSError as e: | 
|  | err("Failed to run '{}': {}".format(cmd_s, e)) | 
|  |  | 
|  | stdout, stderr = process.communicate() | 
|  | # errors="ignore" temporarily works around | 
|  | # https://github.com/zephyrproject-rtos/esp-idf/pull/2 | 
|  | stdout = stdout.decode("utf-8", errors="ignore") | 
|  | stderr = stderr.decode("utf-8") | 
|  | if check and process.returncode: | 
|  | err("""\ | 
|  | '{}' exited with status {}. | 
|  |  | 
|  | ===stdout=== | 
|  | {} | 
|  | ===stderr=== | 
|  | {}""".format(cmd_s, process.returncode, stdout, stderr)) | 
|  |  | 
|  | if stderr: | 
|  | warn("'{}' wrote to stderr:\n{}".format(cmd_s, stderr)) | 
|  |  | 
|  | return stdout | 
|  |  | 
|  |  | 
|  | def err(msg): | 
|  | sys.exit(executable() + "error: " + msg) | 
|  |  | 
|  |  | 
|  | def warn(msg): | 
|  | print(executable() + "warning: " + msg, file=sys.stderr) | 
|  |  | 
|  |  | 
|  | def executable(): | 
|  | cmd = sys.argv[0]  # Empty string if missing | 
|  | return cmd + ": " if cmd else "" | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main() |