kelvin.zhang | ac22e65 | 2021-10-18 15:09:21 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Copyright (c) 2019 Nordic Semiconductor ASA |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
| 5 | |
| 6 | """ |
| 7 | Linter for the Zephyr Kconfig files. Pass --help to see |
| 8 | available checks. By default, all checks are enabled. |
| 9 | |
| 10 | Some of the checks rely on heuristics and can get tripped up |
| 11 | by things like preprocessor magic, so manual checking is |
| 12 | still needed. 'git grep' is handy. |
| 13 | |
| 14 | Requires west, because the checks need to see Kconfig files |
| 15 | and source code from modules. |
| 16 | """ |
| 17 | |
| 18 | import argparse |
| 19 | import os |
| 20 | import re |
| 21 | import shlex |
| 22 | import subprocess |
| 23 | import sys |
| 24 | import tempfile |
| 25 | |
| 26 | TOP_DIR = os.path.join(os.path.dirname(__file__), "..", "..") |
| 27 | |
| 28 | sys.path.insert(0, os.path.join(TOP_DIR, "scripts", "kconfig")) |
| 29 | import kconfiglib |
| 30 | |
| 31 | |
| 32 | def 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 | |
| 54 | def 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__) |
| 60 | |
| 61 | parser.add_argument( |
| 62 | "-n", "--check-always-n", |
| 63 | action="append_const", dest="checks", const=check_always_n, |
| 64 | help="""\ |
| 65 | List symbols that can never be anything but n/empty. These |
| 66 | are detected as symbols with no prompt or defaults that |
| 67 | aren'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="""\ |
| 74 | List symbols that might be unused. |
| 75 | |
| 76 | Heuristic: |
| 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 | |
| 84 | C 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="""\ |
| 90 | List symbols defined with 'menuconfig' where the menu is |
| 91 | empty due to the symbol not being followed by stuff that |
| 92 | depends 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="""\ |
| 98 | List symbols that are only defined in Kconfig.defconfig |
| 99 | files. A common base definition should probably be added |
| 100 | somewhere 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="""\ |
| 107 | Look for references like |
| 108 | |
| 109 | #if MACRO |
| 110 | #if(n)def MACRO |
| 111 | defined(MACRO) |
| 112 | IS_ENABLED(MACRO) |
| 113 | |
| 114 | where MACRO is the name of a defined Kconfig symbol but |
| 115 | doesn't have a CONFIG_ prefix. Could be a typo. |
| 116 | |
| 117 | Macros that are #define'd somewhere are not flagged.""") |
| 118 | |
| 119 | return parser.parse_args() |
| 120 | |
| 121 | |
| 122 | def 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 | |
| 130 | def 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 | |
| 139 | def 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 | |
| 147 | def 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 | |
| 154 | def 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 | |
| 190 | def 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 | |
| 198 | def print_header(s): |
| 199 | print(s + "\n" + len(s)*"=") |
| 200 | |
| 201 | |
| 202 | def 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 | |
| 218 | def 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 | |
| 228 | def 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 | |
| 235 | def 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 | |
| 244 | def 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 | |
| 269 | def has_prompt(sym): |
| 270 | return any(node.prompt for node in sym.nodes) |
| 271 | |
| 272 | |
| 273 | def 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 | |
| 277 | def has_defaults(sym): |
| 278 | return bool(sym.defaults) |
| 279 | |
| 280 | |
| 281 | def is_selecting_or_implying(sym): |
| 282 | return sym.selects or sym.implies |
| 283 | |
| 284 | |
| 285 | def 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 | |
| 293 | def 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 | |
| 327 | def err(msg): |
| 328 | sys.exit(executable() + "error: " + msg) |
| 329 | |
| 330 | |
| 331 | def warn(msg): |
| 332 | print(executable() + "warning: " + msg, file=sys.stderr) |
| 333 | |
| 334 | |
| 335 | def executable(): |
| 336 | cmd = sys.argv[0] # Empty string if missing |
| 337 | return cmd + ": " if cmd else "" |
| 338 | |
| 339 | |
| 340 | if __name__ == "__main__": |
| 341 | main() |