blob: fa784735ee9700fb1a144ac96d80131f8333bab4 [file] [log] [blame]
kelvin.zhangac22e652021-10-18 15:09:21 +08001#!/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__)
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()