scripts: kconfig: lint.py: Add check for missing CONFIG_ prefix

Add a new --check-missing-config-prefix check that looks 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.

Skip MACRO if it is #define'd somewhere, even if it looks like a Kconfig
symbol.

Found e.g. https://github.com/zephyrproject-rtos/zephyr/pull/22195.

Piggyback skipping binary files when grepping for Kconfig symbol
references.

Signed-off-by: Ulf Magnusson <Ulf.Magnusson@nordicsemi.no>
This commit is contained in:
Ulf Magnusson 2020-01-27 06:31:03 +01:00 committed by Carles Cufí
commit e2f647b08b

View file

@ -40,7 +40,8 @@ def main():
checks = (check_always_n, checks = (check_always_n,
check_unused, check_unused,
check_pointless_menuconfigs, check_pointless_menuconfigs,
check_defconfig_only_definition) check_defconfig_only_definition,
check_missing_config_prefix)
first = True first = True
for check in checks: for check in checks:
@ -99,6 +100,22 @@ files. A common base definition should probably be added
somewhere for such symbols, and the type declaration ('int', somewhere for such symbols, and the type declaration ('int',
'hex', etc.) removed from Kconfig.defconfig.""") '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() return parser.parse_args()
@ -134,6 +151,50 @@ def check_defconfig_only_definition():
print(name_and_locs(sym)) 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): def print_header(s):
print(s + "\n" + len(s)*"=") print(s + "\n" + len(s)*"=")
@ -189,7 +250,7 @@ def referenced_outside_kconfig():
# 'git grep' all modules # 'git grep' all modules
for modpath in run(("west", "list", "-f{abspath}")).splitlines(): for modpath in run(("west", "list", "-f{abspath}")).splitlines():
for line in run(("git", "grep", "-h", "--extended-regexp", regex), for line in run(("git", "grep", "-h", "-I", "--extended-regexp", regex),
cwd=modpath).splitlines(): cwd=modpath).splitlines():
# Don't record lines starting with "CONFIG_FOO=" or "# CONFIG_FOO=" # Don't record lines starting with "CONFIG_FOO=" or "# CONFIG_FOO="
# as references, so that symbols that are only assigned in .config # as references, so that symbols that are only assigned in .config
@ -229,9 +290,11 @@ def name_and_locs(sym):
", ".join("{0.filename}:{0.linenr}".format(node) for node in sym.nodes)) ", ".join("{0.filename}:{0.linenr}".format(node) for node in sym.nodes))
def run(cmd, cwd=TOP_DIR): def run(cmd, cwd=TOP_DIR, check=True):
# Runs 'cmd' with subprocess, returning the decoded stdout output. 'cwd' is # Runs 'cmd' with subprocess, returning the decoded stdout output. 'cwd' is
# the working directory. It defaults to the top-level Zephyr directory. # 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) cmd_s = " ".join(shlex.quote(word) for word in cmd)
@ -242,9 +305,11 @@ def run(cmd, cwd=TOP_DIR):
err("Failed to run '{}': {}".format(cmd_s, e)) err("Failed to run '{}': {}".format(cmd_s, e))
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
stdout = stdout.decode("utf-8") # 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") stderr = stderr.decode("utf-8")
if process.returncode: if check and process.returncode:
err("""\ err("""\
'{}' exited with status {}. '{}' exited with status {}.