1#!/usr/bin/env python3 2 3# Copyright (c) 2019 Nordic Semiconductor ASA 4# SPDX-License-Identifier: Apache-2.0 5 6""" 7Linter for the Zephyr Kconfig files. Pass --help to see 8available checks. By default, all checks are enabled. 9 10Some of the checks rely on heuristics and can get tripped up 11by things like preprocessor magic, so manual checking is 12still needed. 'git grep' is handy. 13 14Requires west, because the checks need to see Kconfig files 15and source code from modules. 16""" 17 18import argparse 19import os 20import re 21import shlex 22import subprocess 23import sys 24import tempfile 25 26TOP_DIR = os.path.join(os.path.dirname(__file__), "..", "..") 27 28sys.path.insert(0, os.path.join(TOP_DIR, "scripts", "kconfig")) 29import kconfiglib 30 31 32def main(): 33 init_kconfig() 34 35 args = parse_args() 36 if args.checks: 37 checks = args.checks 38 else: 39 # Run all checks if no checks were specified 40 checks = (check_always_n, 41 check_unused, 42 check_pointless_menuconfigs, 43 check_defconfig_only_definition, 44 check_missing_config_prefix) 45 46 first = True 47 for check in checks: 48 if not first: 49 print() 50 first = False 51 check() 52 53 54def parse_args(): 55 # args.checks is set to a list of check functions to run 56 57 parser = argparse.ArgumentParser( 58 formatter_class=argparse.RawTextHelpFormatter, 59 description=__doc__, allow_abbrev=False) 60 61 parser.add_argument( 62 "-n", "--check-always-n", 63 action="append_const", dest="checks", const=check_always_n, 64 help="""\ 65List symbols that can never be anything but n/empty. These 66are detected as symbols with no prompt or defaults that 67aren't selected or implied. 68""") 69 70 parser.add_argument( 71 "-u", "--check-unused", 72 action="append_const", dest="checks", const=check_unused, 73 help="""\ 74List symbols that might be unused. 75 76Heuristic: 77 78 - Isn't referenced in Kconfig 79 - Isn't referenced as CONFIG_<NAME> outside Kconfig 80 (besides possibly as CONFIG_<NAME>=<VALUE>) 81 - Isn't selecting/implying other symbols 82 - Isn't a choice symbol 83 84C preprocessor magic can trip up this check.""") 85 86 parser.add_argument( 87 "-m", "--check-pointless-menuconfigs", 88 action="append_const", dest="checks", const=check_pointless_menuconfigs, 89 help="""\ 90List symbols defined with 'menuconfig' where the menu is 91empty due to the symbol not being followed by stuff that 92depends on it""") 93 94 parser.add_argument( 95 "-d", "--check-defconfig-only-definition", 96 action="append_const", dest="checks", const=check_defconfig_only_definition, 97 help="""\ 98List symbols that are only defined in Kconfig.defconfig 99files. A common base definition should probably be added 100somewhere for such symbols, and the type declaration ('int', 101'hex', etc.) removed from Kconfig.defconfig.""") 102 103 parser.add_argument( 104 "-p", "--check-missing-config-prefix", 105 action="append_const", dest="checks", const=check_missing_config_prefix, 106 help="""\ 107Look for references like 108 109 #if MACRO 110 #if(n)def MACRO 111 defined(MACRO) 112 IS_ENABLED(MACRO) 113 114where MACRO is the name of a defined Kconfig symbol but 115doesn't have a CONFIG_ prefix. Could be a typo. 116 117Macros that are #define'd somewhere are not flagged.""") 118 119 return parser.parse_args() 120 121 122def check_always_n(): 123 print_header("Symbols that can't be anything but n/empty") 124 for sym in kconf.unique_defined_syms: 125 if not has_prompt(sym) and not is_selected_or_implied(sym) and \ 126 not has_defaults(sym): 127 print(name_and_locs(sym)) 128 129 130def check_unused(): 131 print_header("Symbols that look unused") 132 referenced = referenced_sym_names() 133 for sym in kconf.unique_defined_syms: 134 if not is_selecting_or_implying(sym) and not sym.choice and \ 135 sym.name not in referenced: 136 print(name_and_locs(sym)) 137 138 139def check_pointless_menuconfigs(): 140 print_header("menuconfig symbols with empty menus") 141 for node in kconf.node_iter(): 142 if node.is_menuconfig and not node.list and \ 143 isinstance(node.item, kconfiglib.Symbol): 144 print("{0.item.name:40} {0.filename}:{0.linenr}".format(node)) 145 146 147def check_defconfig_only_definition(): 148 print_header("Symbols only defined in Kconfig.defconfig files") 149 for sym in kconf.unique_defined_syms: 150 if all("defconfig" in node.filename for node in sym.nodes): 151 print(name_and_locs(sym)) 152 153 154def check_missing_config_prefix(): 155 print_header("Symbol references that might be missing a CONFIG_ prefix") 156 157 # Paths to modules 158 modpaths = run(("west", "list", "-f{abspath}")).splitlines() 159 160 # Gather #define'd macros that might overlap with symbol names, so that 161 # they don't trigger false positives 162 defined = set() 163 for modpath in modpaths: 164 regex = r"#\s*define\s+([A-Z0-9_]+)\b" 165 defines = run(("git", "grep", "--extended-regexp", regex), 166 cwd=modpath, check=False) 167 # Could pass --only-matching to git grep as well, but it was added 168 # pretty recently (2018) 169 defined.update(re.findall(regex, defines)) 170 171 # Filter out symbols whose names are #define'd too. Preserve definition 172 # order to make the output consistent. 173 syms = [sym for sym in kconf.unique_defined_syms 174 if sym.name not in defined] 175 176 # grep for symbol references in #ifdef/defined() that are missing a CONFIG_ 177 # prefix. Work around an "argument list too long" error from 'git grep' by 178 # checking symbols in batches. 179 for batch in split_list(syms, 200): 180 # grep for '#if((n)def) <symbol>', 'defined(<symbol>', and 181 # 'IS_ENABLED(<symbol>', with a missing CONFIG_ prefix 182 regex = r"(?:#\s*if(?:n?def)\s+|\bdefined\s*\(\s*|IS_ENABLED\(\s*)(?:" + \ 183 "|".join(sym.name for sym in batch) + r")\b" 184 cmd = ("git", "grep", "--line-number", "-I", "--perl-regexp", regex) 185 186 for modpath in modpaths: 187 print(run(cmd, cwd=modpath, check=False), end="") 188 189 190def split_list(lst, batch_size): 191 # check_missing_config_prefix() helper generator that splits a list into 192 # equal-sized batches (possibly with a shorter batch at the end) 193 194 for i in range(0, len(lst), batch_size): 195 yield lst[i:i + batch_size] 196 197 198def print_header(s): 199 print(s + "\n" + len(s)*"=") 200 201 202def init_kconfig(): 203 global kconf 204 205 os.environ.update( 206 srctree=TOP_DIR, 207 CMAKE_BINARY_DIR=modules_file_dir(), 208 KCONFIG_DOC_MODE="1", 209 ZEPHYR_BASE=TOP_DIR, 210 SOC_DIR="soc", 211 ARCH_DIR="arch", 212 BOARD_DIR="boards/*/*", 213 ARCH="*") 214 215 kconf = kconfiglib.Kconfig(suppress_traceback=True) 216 217 218def modules_file_dir(): 219 # Creates Kconfig.modules in a temporary directory and returns the path to 220 # the directory. Kconfig.modules brings in Kconfig files from modules. 221 222 tmpdir = tempfile.mkdtemp() 223 run((os.path.join("scripts", "zephyr_module.py"), 224 "--kconfig-out", os.path.join(tmpdir, "Kconfig.modules"))) 225 return tmpdir 226 227 228def referenced_sym_names(): 229 # Returns the names of all symbols referenced inside and outside the 230 # Kconfig files (that we can detect), without any "CONFIG_" prefix 231 232 return referenced_in_kconfig() | referenced_outside_kconfig() 233 234 235def referenced_in_kconfig(): 236 # Returns the names of all symbols referenced inside the Kconfig files 237 238 return {ref.name 239 for node in kconf.node_iter() 240 for ref in node.referenced 241 if isinstance(ref, kconfiglib.Symbol)} 242 243 244def referenced_outside_kconfig(): 245 # Returns the names of all symbols referenced outside the Kconfig files 246 247 regex = r"\bCONFIG_[A-Z0-9_]+\b" 248 249 res = set() 250 251 # 'git grep' all modules 252 for modpath in run(("west", "list", "-f{abspath}")).splitlines(): 253 for line in run(("git", "grep", "-h", "-I", "--extended-regexp", regex), 254 cwd=modpath).splitlines(): 255 # Don't record lines starting with "CONFIG_FOO=" or "# CONFIG_FOO=" 256 # as references, so that symbols that are only assigned in .config 257 # files are not included 258 if re.match(r"[\s#]*CONFIG_[A-Z0-9_]+=.*", line): 259 continue 260 261 # Could pass --only-matching to git grep as well, but it was added 262 # pretty recently (2018) 263 for match in re.findall(regex, line): 264 res.add(match[7:]) # Strip "CONFIG_" 265 266 return res 267 268 269def has_prompt(sym): 270 return any(node.prompt for node in sym.nodes) 271 272 273def is_selected_or_implied(sym): 274 return sym.rev_dep is not kconf.n or sym.weak_rev_dep is not kconf.n 275 276 277def has_defaults(sym): 278 return bool(sym.defaults) 279 280 281def is_selecting_or_implying(sym): 282 return sym.selects or sym.implies 283 284 285def name_and_locs(sym): 286 # Returns a string with the name and definition location(s) for 'sym' 287 288 return "{:40} {}".format( 289 sym.name, 290 ", ".join("{0.filename}:{0.linenr}".format(node) for node in sym.nodes)) 291 292 293def run(cmd, cwd=TOP_DIR, check=True): 294 # Runs 'cmd' with subprocess, returning the decoded stdout output. 'cwd' is 295 # the working directory. It defaults to the top-level Zephyr directory. 296 # Exits with an error if the command exits with a non-zero return code if 297 # 'check' is True. 298 299 cmd_s = " ".join(shlex.quote(word) for word in cmd) 300 301 try: 302 process = subprocess.Popen( 303 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) 304 except OSError as e: 305 err("Failed to run '{}': {}".format(cmd_s, e)) 306 307 stdout, stderr = process.communicate() 308 # errors="ignore" temporarily works around 309 # https://github.com/zephyrproject-rtos/esp-idf/pull/2 310 stdout = stdout.decode("utf-8", errors="ignore") 311 stderr = stderr.decode("utf-8") 312 if check and process.returncode: 313 err("""\ 314'{}' exited with status {}. 315 316===stdout=== 317{} 318===stderr=== 319{}""".format(cmd_s, process.returncode, stdout, stderr)) 320 321 if stderr: 322 warn("'{}' wrote to stderr:\n{}".format(cmd_s, stderr)) 323 324 return stdout 325 326 327def err(msg): 328 sys.exit(executable() + "error: " + msg) 329 330 331def warn(msg): 332 print(executable() + "warning: " + msg, file=sys.stderr) 333 334 335def executable(): 336 cmd = sys.argv[0] # Empty string if missing 337 return cmd + ": " if cmd else "" 338 339 340if __name__ == "__main__": 341 main() 342