diff options
Diffstat (limited to 'scripts/checkkconfigsymbols.py')
-rwxr-xr-x | scripts/checkkconfigsymbols.py | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/scripts/checkkconfigsymbols.py b/scripts/checkkconfigsymbols.py new file mode 100755 index 000000000000..2f4b7ffd5570 --- /dev/null +++ b/scripts/checkkconfigsymbols.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python2 + +"""Find Kconfig symbols that are referenced but not defined.""" + +# (c) 2014-2015 Valentin Rothberg <valentinrothberg@gmail.com> +# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de> +# +# Licensed under the terms of the GNU GPL License version 2 + + +import os +import re +import sys +from subprocess import Popen, PIPE, STDOUT +from optparse import OptionParser + + +# regex expressions +OPERATORS = r"&|\(|\)|\||\!" +FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}" +DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*" +EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+" +DEFAULT = r"default\s+.*?(?:if\s.+){,1}" +STMT = r"^\s*(?:if|select|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR +SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")" + +# regex objects +REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") +REGEX_FEATURE = re.compile(r'(?!\B"[^"]*)' + FEATURE + r'(?![^"]*"\B)') +REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE) +REGEX_KCONFIG_DEF = re.compile(DEF) +REGEX_KCONFIG_EXPR = re.compile(EXPR) +REGEX_KCONFIG_STMT = re.compile(STMT) +REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$") +REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$") +REGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+") + + +def parse_options(): + """The user interface of this module.""" + usage = "%prog [options]\n\n" \ + "Run this tool to detect Kconfig symbols that are referenced but " \ + "not defined in\nKconfig. The output of this tool has the " \ + "format \'Undefined symbol\\tFile list\'\n\n" \ + "If no option is specified, %prog will default to check your\n" \ + "current tree. Please note that specifying commits will " \ + "\'git reset --hard\'\nyour current tree! You may save " \ + "uncommitted changes to avoid losing data." + + parser = OptionParser(usage=usage) + + parser.add_option('-c', '--commit', dest='commit', action='store', + default="", + help="Check if the specified commit (hash) introduces " + "undefined Kconfig symbols.") + + parser.add_option('-d', '--diff', dest='diff', action='store', + default="", + help="Diff undefined symbols between two commits. The " + "input format bases on Git log's " + "\'commmit1..commit2\'.") + + parser.add_option('-f', '--find', dest='find', action='store_true', + default=False, + help="Find and show commits that may cause symbols to be " + "missing. Required to run with --diff.") + + parser.add_option('-i', '--ignore', dest='ignore', action='store', + default="", + help="Ignore files matching this pattern. Note that " + "the pattern needs to be a Python regex. To " + "ignore defconfigs, specify -i '.*defconfig'.") + + parser.add_option('', '--force', dest='force', action='store_true', + default=False, + help="Reset current Git tree even when it's dirty.") + + (opts, _) = parser.parse_args() + + if opts.commit and opts.diff: + sys.exit("Please specify only one option at once.") + + if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): + sys.exit("Please specify valid input in the following format: " + "\'commmit1..commit2\'") + + if opts.commit or opts.diff: + if not opts.force and tree_is_dirty(): + sys.exit("The current Git tree is dirty (see 'git status'). " + "Running this script may\ndelete important data since it " + "calls 'git reset --hard' for some performance\nreasons. " + " Please run this script in a clean Git tree or pass " + "'--force' if you\nwant to ignore this warning and " + "continue.") + + if opts.commit: + opts.find = False + + if opts.ignore: + try: + re.match(opts.ignore, "this/is/just/a/test.c") + except: + sys.exit("Please specify a valid Python regex.") + + return opts + + +def main(): + """Main function of this module.""" + opts = parse_options() + + if opts.commit or opts.diff: + head = get_head() + + # get commit range + commit_a = None + commit_b = None + if opts.commit: + commit_a = opts.commit + "~" + commit_b = opts.commit + elif opts.diff: + split = opts.diff.split("..") + commit_a = split[0] + commit_b = split[1] + undefined_a = {} + undefined_b = {} + + # get undefined items before the commit + execute("git reset --hard %s" % commit_a) + undefined_a = check_symbols(opts.ignore) + + # get undefined items for the commit + execute("git reset --hard %s" % commit_b) + undefined_b = check_symbols(opts.ignore) + + # report cases that are present for the commit but not before + for feature in sorted(undefined_b): + # feature has not been undefined before + if not feature in undefined_a: + files = sorted(undefined_b.get(feature)) + print "%s\t%s" % (yel(feature), ", ".join(files)) + if opts.find: + commits = find_commits(feature, opts.diff) + print red(commits) + # check if there are new files that reference the undefined feature + else: + files = sorted(undefined_b.get(feature) - + undefined_a.get(feature)) + if files: + print "%s\t%s" % (yel(feature), ", ".join(files)) + if opts.find: + commits = find_commits(feature, opts.diff) + print red(commits) + + # reset to head + execute("git reset --hard %s" % head) + + # default to check the entire tree + else: + undefined = check_symbols(opts.ignore) + for feature in sorted(undefined): + files = sorted(undefined.get(feature)) + print "%s\t%s" % (yel(feature), ", ".join(files)) + + +def yel(string): + """ + Color %string yellow. + """ + return "\033[33m%s\033[0m" % string + + +def red(string): + """ + Color %string red. + """ + return "\033[31m%s\033[0m" % string + + +def execute(cmd): + """Execute %cmd and return stdout. Exit in case of error.""" + pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) + (stdout, _) = pop.communicate() # wait until finished + if pop.returncode != 0: + sys.exit(stdout) + return stdout + + +def find_commits(symbol, diff): + """Find commits changing %symbol in the given range of %diff.""" + commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" + % (symbol, diff)) + return commits + + +def tree_is_dirty(): + """Return true if the current working tree is dirty (i.e., if any file has + been added, deleted, modified, renamed or copied but not committed).""" + stdout = execute("git status --porcelain") + for line in stdout: + if re.findall(r"[URMADC]{1}", line[:2]): + return True + return False + + +def get_head(): + """Return commit hash of current HEAD.""" + stdout = execute("git rev-parse HEAD") + return stdout.strip('\n') + + +def check_symbols(ignore): + """Find undefined Kconfig symbols and return a dict with the symbol as key + and a list of referencing files as value. Files matching %ignore are not + checked for undefined symbols.""" + source_files = [] + kconfig_files = [] + defined_features = set() + referenced_features = dict() # {feature: [files]} + + # use 'git ls-files' to get the worklist + stdout = execute("git ls-files") + if len(stdout) > 0 and stdout[-1] == "\n": + stdout = stdout[:-1] + + for gitfile in stdout.rsplit("\n"): + if ".git" in gitfile or "ChangeLog" in gitfile or \ + ".log" in gitfile or os.path.isdir(gitfile) or \ + gitfile.startswith("tools/"): + continue + if REGEX_FILE_KCONFIG.match(gitfile): + kconfig_files.append(gitfile) + else: + # all non-Kconfig files are checked for consistency + source_files.append(gitfile) + + for sfile in source_files: + if ignore and re.match(ignore, sfile): + # do not check files matching %ignore + continue + parse_source_file(sfile, referenced_features) + + for kfile in kconfig_files: + if ignore and re.match(ignore, kfile): + # do not collect references for files matching %ignore + parse_kconfig_file(kfile, defined_features, dict()) + else: + parse_kconfig_file(kfile, defined_features, referenced_features) + + undefined = {} # {feature: [files]} + for feature in sorted(referenced_features): + # filter some false positives + if feature == "FOO" or feature == "BAR" or \ + feature == "FOO_BAR" or feature == "XXX": + continue + if feature not in defined_features: + if feature.endswith("_MODULE"): + # avoid false positives for kernel modules + if feature[:-len("_MODULE")] in defined_features: + continue + undefined[feature] = referenced_features.get(feature) + return undefined + + +def parse_source_file(sfile, referenced_features): + """Parse @sfile for referenced Kconfig features.""" + lines = [] + with open(sfile, "r") as stream: + lines = stream.readlines() + + for line in lines: + if not "CONFIG_" in line: + continue + features = REGEX_SOURCE_FEATURE.findall(line) + for feature in features: + if not REGEX_FILTER_FEATURES.search(feature): + continue + sfiles = referenced_features.get(feature, set()) + sfiles.add(sfile) + referenced_features[feature] = sfiles + + +def get_features_in_line(line): + """Return mentioned Kconfig features in @line.""" + return REGEX_FEATURE.findall(line) + + +def parse_kconfig_file(kfile, defined_features, referenced_features): + """Parse @kfile and update feature definitions and references.""" + lines = [] + skip = False + + with open(kfile, "r") as stream: + lines = stream.readlines() + + for i in range(len(lines)): + line = lines[i] + line = line.strip('\n') + line = line.split("#")[0] # ignore comments + + if REGEX_KCONFIG_DEF.match(line): + feature_def = REGEX_KCONFIG_DEF.findall(line) + defined_features.add(feature_def[0]) + skip = False + elif REGEX_KCONFIG_HELP.match(line): + skip = True + elif skip: + # ignore content of help messages + pass + elif REGEX_KCONFIG_STMT.match(line): + features = get_features_in_line(line) + # multi-line statements + while line.endswith("\\"): + i += 1 + line = lines[i] + line = line.strip('\n') + features.extend(get_features_in_line(line)) + for feature in set(features): + if REGEX_NUMERIC.match(feature): + # ignore numeric values + continue + paths = referenced_features.get(feature, set()) + paths.add(kfile) + referenced_features[feature] = paths + + +if __name__ == "__main__": + main() |