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