| #!/usr/bin/env python2 |
| |
| """Find Kconfig symbols that are referenced but not defined.""" |
| |
| # (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr> |
| # (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")+" |
| STMT = r"^\s*(?:if|select|depends\s+on)\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"(" + FEATURE + r")") |
| 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]$") |
| |
| |
| 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('-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.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" % (feature, ", ".join(files)) |
| # 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" % (feature, ", ".join(files)) |
| |
| # 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" % (feature, ", ".join(files)) |
| |
| |
| 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 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): |
| paths = referenced_features.get(feature, set()) |
| paths.add(kfile) |
| referenced_features[feature] = paths |
| |
| |
| if __name__ == "__main__": |
| main() |