| #!/usr/bin/env python | 
 |  | 
 | """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('', '--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.") | 
 |  | 
 |     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() | 
 |  | 
 |         # get undefined items for the commit | 
 |         execute("git reset --hard %s" % commit_b) | 
 |         undefined_b = check_symbols() | 
 |  | 
 |         # 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() | 
 |         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(): | 
 |     """Find undefined Kconfig symbols and return a dict with the symbol as key | 
 |     and a list of referencing files as value.""" | 
 |     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: | 
 |         parse_source_file(sfile, referenced_features) | 
 |  | 
 |     for kfile in kconfig_files: | 
 |         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() |