14b6fda0bSValentin Rothberg#!/usr/bin/env python2 224fe1f03SValentin Rothberg 3b1a3f243SValentin Rothberg"""Find Kconfig symbols that are referenced but not defined.""" 424fe1f03SValentin Rothberg 5208d5115SValentin Rothberg# (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr> 6cc641d55SValentin Rothberg# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de> 724fe1f03SValentin Rothberg# 8cc641d55SValentin Rothberg# Licensed under the terms of the GNU GPL License version 2 924fe1f03SValentin Rothberg 1024fe1f03SValentin Rothberg 1124fe1f03SValentin Rothbergimport os 1224fe1f03SValentin Rothbergimport re 13b1a3f243SValentin Rothbergimport sys 1424fe1f03SValentin Rothbergfrom subprocess import Popen, PIPE, STDOUT 15b1a3f243SValentin Rothbergfrom optparse import OptionParser 1624fe1f03SValentin Rothberg 17cc641d55SValentin Rothberg 18cc641d55SValentin Rothberg# regex expressions 1924fe1f03SValentin RothbergOPERATORS = r"&|\(|\)|\||\!" 20cc641d55SValentin RothbergFEATURE = r"(?:\w*[A-Z0-9]\w*){2,}" 21cc641d55SValentin RothbergDEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*" 2224fe1f03SValentin RothbergEXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+" 2324fe1f03SValentin RothbergSTMT = r"^\s*(?:if|select|depends\s+on)\s+" + EXPR 24cc641d55SValentin RothbergSOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")" 2524fe1f03SValentin Rothberg 26cc641d55SValentin Rothberg# regex objects 2724fe1f03SValentin RothbergREGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") 2824fe1f03SValentin RothbergREGEX_FEATURE = re.compile(r"(" + FEATURE + r")") 29cc641d55SValentin RothbergREGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE) 30cc641d55SValentin RothbergREGEX_KCONFIG_DEF = re.compile(DEF) 3124fe1f03SValentin RothbergREGEX_KCONFIG_EXPR = re.compile(EXPR) 3224fe1f03SValentin RothbergREGEX_KCONFIG_STMT = re.compile(STMT) 3324fe1f03SValentin RothbergREGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$") 3424fe1f03SValentin RothbergREGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$") 3524fe1f03SValentin Rothberg 3624fe1f03SValentin Rothberg 37b1a3f243SValentin Rothbergdef parse_options(): 38b1a3f243SValentin Rothberg """The user interface of this module.""" 39b1a3f243SValentin Rothberg usage = "%prog [options]\n\n" \ 40b1a3f243SValentin Rothberg "Run this tool to detect Kconfig symbols that are referenced but " \ 41b1a3f243SValentin Rothberg "not defined in\nKconfig. The output of this tool has the " \ 42b1a3f243SValentin Rothberg "format \'Undefined symbol\\tFile list\'\n\n" \ 43b1a3f243SValentin Rothberg "If no option is specified, %prog will default to check your\n" \ 44b1a3f243SValentin Rothberg "current tree. Please note that specifying commits will " \ 45b1a3f243SValentin Rothberg "\'git reset --hard\'\nyour current tree! You may save " \ 46b1a3f243SValentin Rothberg "uncommitted changes to avoid losing data." 47b1a3f243SValentin Rothberg 48b1a3f243SValentin Rothberg parser = OptionParser(usage=usage) 49b1a3f243SValentin Rothberg 50b1a3f243SValentin Rothberg parser.add_option('-c', '--commit', dest='commit', action='store', 51b1a3f243SValentin Rothberg default="", 52b1a3f243SValentin Rothberg help="Check if the specified commit (hash) introduces " 53b1a3f243SValentin Rothberg "undefined Kconfig symbols.") 54b1a3f243SValentin Rothberg 55b1a3f243SValentin Rothberg parser.add_option('-d', '--diff', dest='diff', action='store', 56b1a3f243SValentin Rothberg default="", 57b1a3f243SValentin Rothberg help="Diff undefined symbols between two commits. The " 58b1a3f243SValentin Rothberg "input format bases on Git log's " 59b1a3f243SValentin Rothberg "\'commmit1..commit2\'.") 60b1a3f243SValentin Rothberg 61a42fa92cSValentin Rothberg parser.add_option('-f', '--find', dest='find', action='store_true', 62a42fa92cSValentin Rothberg default=False, 63a42fa92cSValentin Rothberg help="Find and show commits that may cause symbols to be " 64a42fa92cSValentin Rothberg "missing. Required to run with --diff.") 65a42fa92cSValentin Rothberg 66cf132e4aSValentin Rothberg parser.add_option('-i', '--ignore', dest='ignore', action='store', 67cf132e4aSValentin Rothberg default="", 68cf132e4aSValentin Rothberg help="Ignore files matching this pattern. Note that " 69cf132e4aSValentin Rothberg "the pattern needs to be a Python regex. To " 70cf132e4aSValentin Rothberg "ignore defconfigs, specify -i '.*defconfig'.") 71cf132e4aSValentin Rothberg 72b1a3f243SValentin Rothberg parser.add_option('', '--force', dest='force', action='store_true', 73b1a3f243SValentin Rothberg default=False, 74b1a3f243SValentin Rothberg help="Reset current Git tree even when it's dirty.") 75b1a3f243SValentin Rothberg 76b1a3f243SValentin Rothberg (opts, _) = parser.parse_args() 77b1a3f243SValentin Rothberg 78b1a3f243SValentin Rothberg if opts.commit and opts.diff: 79b1a3f243SValentin Rothberg sys.exit("Please specify only one option at once.") 80b1a3f243SValentin Rothberg 81b1a3f243SValentin Rothberg if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): 82b1a3f243SValentin Rothberg sys.exit("Please specify valid input in the following format: " 83b1a3f243SValentin Rothberg "\'commmit1..commit2\'") 84b1a3f243SValentin Rothberg 85b1a3f243SValentin Rothberg if opts.commit or opts.diff: 86b1a3f243SValentin Rothberg if not opts.force and tree_is_dirty(): 87b1a3f243SValentin Rothberg sys.exit("The current Git tree is dirty (see 'git status'). " 88b1a3f243SValentin Rothberg "Running this script may\ndelete important data since it " 89b1a3f243SValentin Rothberg "calls 'git reset --hard' for some performance\nreasons. " 90b1a3f243SValentin Rothberg " Please run this script in a clean Git tree or pass " 91b1a3f243SValentin Rothberg "'--force' if you\nwant to ignore this warning and " 92b1a3f243SValentin Rothberg "continue.") 93b1a3f243SValentin Rothberg 94a42fa92cSValentin Rothberg if opts.commit: 95a42fa92cSValentin Rothberg opts.find = False 96a42fa92cSValentin Rothberg 97cf132e4aSValentin Rothberg if opts.ignore: 98cf132e4aSValentin Rothberg try: 99cf132e4aSValentin Rothberg re.match(opts.ignore, "this/is/just/a/test.c") 100cf132e4aSValentin Rothberg except: 101cf132e4aSValentin Rothberg sys.exit("Please specify a valid Python regex.") 102cf132e4aSValentin Rothberg 103b1a3f243SValentin Rothberg return opts 104b1a3f243SValentin Rothberg 105b1a3f243SValentin Rothberg 10624fe1f03SValentin Rothbergdef main(): 10724fe1f03SValentin Rothberg """Main function of this module.""" 108b1a3f243SValentin Rothberg opts = parse_options() 109b1a3f243SValentin Rothberg 110b1a3f243SValentin Rothberg if opts.commit or opts.diff: 111b1a3f243SValentin Rothberg head = get_head() 112b1a3f243SValentin Rothberg 113b1a3f243SValentin Rothberg # get commit range 114b1a3f243SValentin Rothberg commit_a = None 115b1a3f243SValentin Rothberg commit_b = None 116b1a3f243SValentin Rothberg if opts.commit: 117b1a3f243SValentin Rothberg commit_a = opts.commit + "~" 118b1a3f243SValentin Rothberg commit_b = opts.commit 119b1a3f243SValentin Rothberg elif opts.diff: 120b1a3f243SValentin Rothberg split = opts.diff.split("..") 121b1a3f243SValentin Rothberg commit_a = split[0] 122b1a3f243SValentin Rothberg commit_b = split[1] 123b1a3f243SValentin Rothberg undefined_a = {} 124b1a3f243SValentin Rothberg undefined_b = {} 125b1a3f243SValentin Rothberg 126b1a3f243SValentin Rothberg # get undefined items before the commit 127b1a3f243SValentin Rothberg execute("git reset --hard %s" % commit_a) 128cf132e4aSValentin Rothberg undefined_a = check_symbols(opts.ignore) 129b1a3f243SValentin Rothberg 130b1a3f243SValentin Rothberg # get undefined items for the commit 131b1a3f243SValentin Rothberg execute("git reset --hard %s" % commit_b) 132cf132e4aSValentin Rothberg undefined_b = check_symbols(opts.ignore) 133b1a3f243SValentin Rothberg 134b1a3f243SValentin Rothberg # report cases that are present for the commit but not before 135e9533ae5SValentin Rothberg for feature in sorted(undefined_b): 136b1a3f243SValentin Rothberg # feature has not been undefined before 137b1a3f243SValentin Rothberg if not feature in undefined_a: 138e9533ae5SValentin Rothberg files = sorted(undefined_b.get(feature)) 139b1a3f243SValentin Rothberg print "%s\t%s" % (feature, ", ".join(files)) 140a42fa92cSValentin Rothberg if opts.find: 141a42fa92cSValentin Rothberg commits = find_commits(feature, opts.diff) 142a42fa92cSValentin Rothberg print commits 143b1a3f243SValentin Rothberg # check if there are new files that reference the undefined feature 144b1a3f243SValentin Rothberg else: 145e9533ae5SValentin Rothberg files = sorted(undefined_b.get(feature) - 146e9533ae5SValentin Rothberg undefined_a.get(feature)) 147b1a3f243SValentin Rothberg if files: 148b1a3f243SValentin Rothberg print "%s\t%s" % (feature, ", ".join(files)) 149a42fa92cSValentin Rothberg if opts.find: 150a42fa92cSValentin Rothberg commits = find_commits(feature, opts.diff) 151a42fa92cSValentin Rothberg print commits 152b1a3f243SValentin Rothberg 153b1a3f243SValentin Rothberg # reset to head 154b1a3f243SValentin Rothberg execute("git reset --hard %s" % head) 155b1a3f243SValentin Rothberg 156b1a3f243SValentin Rothberg # default to check the entire tree 157b1a3f243SValentin Rothberg else: 158cf132e4aSValentin Rothberg undefined = check_symbols(opts.ignore) 159e9533ae5SValentin Rothberg for feature in sorted(undefined): 160e9533ae5SValentin Rothberg files = sorted(undefined.get(feature)) 161e9533ae5SValentin Rothberg print "%s\t%s" % (feature, ", ".join(files)) 162b1a3f243SValentin Rothberg 163b1a3f243SValentin Rothberg 164b1a3f243SValentin Rothbergdef execute(cmd): 165b1a3f243SValentin Rothberg """Execute %cmd and return stdout. Exit in case of error.""" 166b1a3f243SValentin Rothberg pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 167b1a3f243SValentin Rothberg (stdout, _) = pop.communicate() # wait until finished 168b1a3f243SValentin Rothberg if pop.returncode != 0: 169b1a3f243SValentin Rothberg sys.exit(stdout) 170b1a3f243SValentin Rothberg return stdout 171b1a3f243SValentin Rothberg 172b1a3f243SValentin Rothberg 173a42fa92cSValentin Rothbergdef find_commits(symbol, diff): 174a42fa92cSValentin Rothberg """Find commits changing %symbol in the given range of %diff.""" 175a42fa92cSValentin Rothberg commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" 176a42fa92cSValentin Rothberg % (symbol, diff)) 177a42fa92cSValentin Rothberg return commits 178a42fa92cSValentin Rothberg 179a42fa92cSValentin Rothberg 180b1a3f243SValentin Rothbergdef tree_is_dirty(): 181b1a3f243SValentin Rothberg """Return true if the current working tree is dirty (i.e., if any file has 182b1a3f243SValentin Rothberg been added, deleted, modified, renamed or copied but not committed).""" 183b1a3f243SValentin Rothberg stdout = execute("git status --porcelain") 184b1a3f243SValentin Rothberg for line in stdout: 185b1a3f243SValentin Rothberg if re.findall(r"[URMADC]{1}", line[:2]): 186b1a3f243SValentin Rothberg return True 187b1a3f243SValentin Rothberg return False 188b1a3f243SValentin Rothberg 189b1a3f243SValentin Rothberg 190b1a3f243SValentin Rothbergdef get_head(): 191b1a3f243SValentin Rothberg """Return commit hash of current HEAD.""" 192b1a3f243SValentin Rothberg stdout = execute("git rev-parse HEAD") 193b1a3f243SValentin Rothberg return stdout.strip('\n') 194b1a3f243SValentin Rothberg 195b1a3f243SValentin Rothberg 196cf132e4aSValentin Rothbergdef check_symbols(ignore): 197b1a3f243SValentin Rothberg """Find undefined Kconfig symbols and return a dict with the symbol as key 198cf132e4aSValentin Rothberg and a list of referencing files as value. Files matching %ignore are not 199cf132e4aSValentin Rothberg checked for undefined symbols.""" 20024fe1f03SValentin Rothberg source_files = [] 20124fe1f03SValentin Rothberg kconfig_files = [] 20224fe1f03SValentin Rothberg defined_features = set() 203cc641d55SValentin Rothberg referenced_features = dict() # {feature: [files]} 20424fe1f03SValentin Rothberg 20524fe1f03SValentin Rothberg # use 'git ls-files' to get the worklist 206b1a3f243SValentin Rothberg stdout = execute("git ls-files") 20724fe1f03SValentin Rothberg if len(stdout) > 0 and stdout[-1] == "\n": 20824fe1f03SValentin Rothberg stdout = stdout[:-1] 20924fe1f03SValentin Rothberg 21024fe1f03SValentin Rothberg for gitfile in stdout.rsplit("\n"): 21124fe1f03SValentin Rothberg if ".git" in gitfile or "ChangeLog" in gitfile or \ 212208d5115SValentin Rothberg ".log" in gitfile or os.path.isdir(gitfile) or \ 213208d5115SValentin Rothberg gitfile.startswith("tools/"): 21424fe1f03SValentin Rothberg continue 21524fe1f03SValentin Rothberg if REGEX_FILE_KCONFIG.match(gitfile): 21624fe1f03SValentin Rothberg kconfig_files.append(gitfile) 21724fe1f03SValentin Rothberg else: 218cc641d55SValentin Rothberg # all non-Kconfig files are checked for consistency 21924fe1f03SValentin Rothberg source_files.append(gitfile) 22024fe1f03SValentin Rothberg 22124fe1f03SValentin Rothberg for sfile in source_files: 222cf132e4aSValentin Rothberg if ignore and re.match(ignore, sfile): 223cf132e4aSValentin Rothberg # do not check files matching %ignore 224cf132e4aSValentin Rothberg continue 22524fe1f03SValentin Rothberg parse_source_file(sfile, referenced_features) 22624fe1f03SValentin Rothberg 22724fe1f03SValentin Rothberg for kfile in kconfig_files: 228cf132e4aSValentin Rothberg if ignore and re.match(ignore, kfile): 229cf132e4aSValentin Rothberg # do not collect references for files matching %ignore 230cf132e4aSValentin Rothberg parse_kconfig_file(kfile, defined_features, dict()) 231cf132e4aSValentin Rothberg else: 23224fe1f03SValentin Rothberg parse_kconfig_file(kfile, defined_features, referenced_features) 23324fe1f03SValentin Rothberg 234b1a3f243SValentin Rothberg undefined = {} # {feature: [files]} 23524fe1f03SValentin Rothberg for feature in sorted(referenced_features): 236cc641d55SValentin Rothberg # filter some false positives 237cc641d55SValentin Rothberg if feature == "FOO" or feature == "BAR" or \ 238cc641d55SValentin Rothberg feature == "FOO_BAR" or feature == "XXX": 239cc641d55SValentin Rothberg continue 24024fe1f03SValentin Rothberg if feature not in defined_features: 24124fe1f03SValentin Rothberg if feature.endswith("_MODULE"): 242cc641d55SValentin Rothberg # avoid false positives for kernel modules 24324fe1f03SValentin Rothberg if feature[:-len("_MODULE")] in defined_features: 24424fe1f03SValentin Rothberg continue 245b1a3f243SValentin Rothberg undefined[feature] = referenced_features.get(feature) 246b1a3f243SValentin Rothberg return undefined 24724fe1f03SValentin Rothberg 24824fe1f03SValentin Rothberg 24924fe1f03SValentin Rothbergdef parse_source_file(sfile, referenced_features): 25024fe1f03SValentin Rothberg """Parse @sfile for referenced Kconfig features.""" 25124fe1f03SValentin Rothberg lines = [] 25224fe1f03SValentin Rothberg with open(sfile, "r") as stream: 25324fe1f03SValentin Rothberg lines = stream.readlines() 25424fe1f03SValentin Rothberg 25524fe1f03SValentin Rothberg for line in lines: 25624fe1f03SValentin Rothberg if not "CONFIG_" in line: 25724fe1f03SValentin Rothberg continue 25824fe1f03SValentin Rothberg features = REGEX_SOURCE_FEATURE.findall(line) 25924fe1f03SValentin Rothberg for feature in features: 26024fe1f03SValentin Rothberg if not REGEX_FILTER_FEATURES.search(feature): 26124fe1f03SValentin Rothberg continue 262cc641d55SValentin Rothberg sfiles = referenced_features.get(feature, set()) 263cc641d55SValentin Rothberg sfiles.add(sfile) 264cc641d55SValentin Rothberg referenced_features[feature] = sfiles 26524fe1f03SValentin Rothberg 26624fe1f03SValentin Rothberg 26724fe1f03SValentin Rothbergdef get_features_in_line(line): 26824fe1f03SValentin Rothberg """Return mentioned Kconfig features in @line.""" 26924fe1f03SValentin Rothberg return REGEX_FEATURE.findall(line) 27024fe1f03SValentin Rothberg 27124fe1f03SValentin Rothberg 27224fe1f03SValentin Rothbergdef parse_kconfig_file(kfile, defined_features, referenced_features): 27324fe1f03SValentin Rothberg """Parse @kfile and update feature definitions and references.""" 27424fe1f03SValentin Rothberg lines = [] 27524fe1f03SValentin Rothberg skip = False 27624fe1f03SValentin Rothberg 27724fe1f03SValentin Rothberg with open(kfile, "r") as stream: 27824fe1f03SValentin Rothberg lines = stream.readlines() 27924fe1f03SValentin Rothberg 28024fe1f03SValentin Rothberg for i in range(len(lines)): 28124fe1f03SValentin Rothberg line = lines[i] 28224fe1f03SValentin Rothberg line = line.strip('\n') 283cc641d55SValentin Rothberg line = line.split("#")[0] # ignore comments 28424fe1f03SValentin Rothberg 28524fe1f03SValentin Rothberg if REGEX_KCONFIG_DEF.match(line): 28624fe1f03SValentin Rothberg feature_def = REGEX_KCONFIG_DEF.findall(line) 28724fe1f03SValentin Rothberg defined_features.add(feature_def[0]) 28824fe1f03SValentin Rothberg skip = False 28924fe1f03SValentin Rothberg elif REGEX_KCONFIG_HELP.match(line): 29024fe1f03SValentin Rothberg skip = True 29124fe1f03SValentin Rothberg elif skip: 292cc641d55SValentin Rothberg # ignore content of help messages 29324fe1f03SValentin Rothberg pass 29424fe1f03SValentin Rothberg elif REGEX_KCONFIG_STMT.match(line): 29524fe1f03SValentin Rothberg features = get_features_in_line(line) 296cc641d55SValentin Rothberg # multi-line statements 29724fe1f03SValentin Rothberg while line.endswith("\\"): 29824fe1f03SValentin Rothberg i += 1 29924fe1f03SValentin Rothberg line = lines[i] 30024fe1f03SValentin Rothberg line = line.strip('\n') 30124fe1f03SValentin Rothberg features.extend(get_features_in_line(line)) 30224fe1f03SValentin Rothberg for feature in set(features): 30324fe1f03SValentin Rothberg paths = referenced_features.get(feature, set()) 30424fe1f03SValentin Rothberg paths.add(kfile) 30524fe1f03SValentin Rothberg referenced_features[feature] = paths 30624fe1f03SValentin Rothberg 30724fe1f03SValentin Rothberg 30824fe1f03SValentin Rothbergif __name__ == "__main__": 30924fe1f03SValentin Rothberg main() 310