1#!/usr/bin/env python2 2 3"""Find Kconfig symbols that are referenced but not defined.""" 4 5# (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr> 6# (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de> 7# 8# Licensed under the terms of the GNU GPL License version 2 9 10 11import os 12import re 13import sys 14from subprocess import Popen, PIPE, STDOUT 15from optparse import OptionParser 16 17 18# regex expressions 19OPERATORS = r"&|\(|\)|\||\!" 20FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}" 21DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*" 22EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+" 23STMT = r"^\s*(?:if|select|depends\s+on)\s+" + EXPR 24SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")" 25 26# regex objects 27REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") 28REGEX_FEATURE = re.compile(r"(" + FEATURE + r")") 29REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE) 30REGEX_KCONFIG_DEF = re.compile(DEF) 31REGEX_KCONFIG_EXPR = re.compile(EXPR) 32REGEX_KCONFIG_STMT = re.compile(STMT) 33REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$") 34REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$") 35 36 37def parse_options(): 38 """The user interface of this module.""" 39 usage = "%prog [options]\n\n" \ 40 "Run this tool to detect Kconfig symbols that are referenced but " \ 41 "not defined in\nKconfig. The output of this tool has the " \ 42 "format \'Undefined symbol\\tFile list\'\n\n" \ 43 "If no option is specified, %prog will default to check your\n" \ 44 "current tree. Please note that specifying commits will " \ 45 "\'git reset --hard\'\nyour current tree! You may save " \ 46 "uncommitted changes to avoid losing data." 47 48 parser = OptionParser(usage=usage) 49 50 parser.add_option('-c', '--commit', dest='commit', action='store', 51 default="", 52 help="Check if the specified commit (hash) introduces " 53 "undefined Kconfig symbols.") 54 55 parser.add_option('-d', '--diff', dest='diff', action='store', 56 default="", 57 help="Diff undefined symbols between two commits. The " 58 "input format bases on Git log's " 59 "\'commmit1..commit2\'.") 60 61 parser.add_option('-i', '--ignore', dest='ignore', action='store', 62 default="", 63 help="Ignore files matching this pattern. Note that " 64 "the pattern needs to be a Python regex. To " 65 "ignore defconfigs, specify -i '.*defconfig'.") 66 67 parser.add_option('', '--force', dest='force', action='store_true', 68 default=False, 69 help="Reset current Git tree even when it's dirty.") 70 71 (opts, _) = parser.parse_args() 72 73 if opts.commit and opts.diff: 74 sys.exit("Please specify only one option at once.") 75 76 if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): 77 sys.exit("Please specify valid input in the following format: " 78 "\'commmit1..commit2\'") 79 80 if opts.commit or opts.diff: 81 if not opts.force and tree_is_dirty(): 82 sys.exit("The current Git tree is dirty (see 'git status'). " 83 "Running this script may\ndelete important data since it " 84 "calls 'git reset --hard' for some performance\nreasons. " 85 " Please run this script in a clean Git tree or pass " 86 "'--force' if you\nwant to ignore this warning and " 87 "continue.") 88 89 if opts.ignore: 90 try: 91 re.match(opts.ignore, "this/is/just/a/test.c") 92 except: 93 sys.exit("Please specify a valid Python regex.") 94 95 return opts 96 97 98def main(): 99 """Main function of this module.""" 100 opts = parse_options() 101 102 if opts.commit or opts.diff: 103 head = get_head() 104 105 # get commit range 106 commit_a = None 107 commit_b = None 108 if opts.commit: 109 commit_a = opts.commit + "~" 110 commit_b = opts.commit 111 elif opts.diff: 112 split = opts.diff.split("..") 113 commit_a = split[0] 114 commit_b = split[1] 115 undefined_a = {} 116 undefined_b = {} 117 118 # get undefined items before the commit 119 execute("git reset --hard %s" % commit_a) 120 undefined_a = check_symbols(opts.ignore) 121 122 # get undefined items for the commit 123 execute("git reset --hard %s" % commit_b) 124 undefined_b = check_symbols(opts.ignore) 125 126 # report cases that are present for the commit but not before 127 for feature in sorted(undefined_b): 128 # feature has not been undefined before 129 if not feature in undefined_a: 130 files = sorted(undefined_b.get(feature)) 131 print "%s\t%s" % (feature, ", ".join(files)) 132 # check if there are new files that reference the undefined feature 133 else: 134 files = sorted(undefined_b.get(feature) - 135 undefined_a.get(feature)) 136 if files: 137 print "%s\t%s" % (feature, ", ".join(files)) 138 139 # reset to head 140 execute("git reset --hard %s" % head) 141 142 # default to check the entire tree 143 else: 144 undefined = check_symbols(opts.ignore) 145 for feature in sorted(undefined): 146 files = sorted(undefined.get(feature)) 147 print "%s\t%s" % (feature, ", ".join(files)) 148 149 150def execute(cmd): 151 """Execute %cmd and return stdout. Exit in case of error.""" 152 pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 153 (stdout, _) = pop.communicate() # wait until finished 154 if pop.returncode != 0: 155 sys.exit(stdout) 156 return stdout 157 158 159def tree_is_dirty(): 160 """Return true if the current working tree is dirty (i.e., if any file has 161 been added, deleted, modified, renamed or copied but not committed).""" 162 stdout = execute("git status --porcelain") 163 for line in stdout: 164 if re.findall(r"[URMADC]{1}", line[:2]): 165 return True 166 return False 167 168 169def get_head(): 170 """Return commit hash of current HEAD.""" 171 stdout = execute("git rev-parse HEAD") 172 return stdout.strip('\n') 173 174 175def check_symbols(ignore): 176 """Find undefined Kconfig symbols and return a dict with the symbol as key 177 and a list of referencing files as value. Files matching %ignore are not 178 checked for undefined symbols.""" 179 source_files = [] 180 kconfig_files = [] 181 defined_features = set() 182 referenced_features = dict() # {feature: [files]} 183 184 # use 'git ls-files' to get the worklist 185 stdout = execute("git ls-files") 186 if len(stdout) > 0 and stdout[-1] == "\n": 187 stdout = stdout[:-1] 188 189 for gitfile in stdout.rsplit("\n"): 190 if ".git" in gitfile or "ChangeLog" in gitfile or \ 191 ".log" in gitfile or os.path.isdir(gitfile) or \ 192 gitfile.startswith("tools/"): 193 continue 194 if REGEX_FILE_KCONFIG.match(gitfile): 195 kconfig_files.append(gitfile) 196 else: 197 # all non-Kconfig files are checked for consistency 198 source_files.append(gitfile) 199 200 for sfile in source_files: 201 if ignore and re.match(ignore, sfile): 202 # do not check files matching %ignore 203 continue 204 parse_source_file(sfile, referenced_features) 205 206 for kfile in kconfig_files: 207 if ignore and re.match(ignore, kfile): 208 # do not collect references for files matching %ignore 209 parse_kconfig_file(kfile, defined_features, dict()) 210 else: 211 parse_kconfig_file(kfile, defined_features, referenced_features) 212 213 undefined = {} # {feature: [files]} 214 for feature in sorted(referenced_features): 215 # filter some false positives 216 if feature == "FOO" or feature == "BAR" or \ 217 feature == "FOO_BAR" or feature == "XXX": 218 continue 219 if feature not in defined_features: 220 if feature.endswith("_MODULE"): 221 # avoid false positives for kernel modules 222 if feature[:-len("_MODULE")] in defined_features: 223 continue 224 undefined[feature] = referenced_features.get(feature) 225 return undefined 226 227 228def parse_source_file(sfile, referenced_features): 229 """Parse @sfile for referenced Kconfig features.""" 230 lines = [] 231 with open(sfile, "r") as stream: 232 lines = stream.readlines() 233 234 for line in lines: 235 if not "CONFIG_" in line: 236 continue 237 features = REGEX_SOURCE_FEATURE.findall(line) 238 for feature in features: 239 if not REGEX_FILTER_FEATURES.search(feature): 240 continue 241 sfiles = referenced_features.get(feature, set()) 242 sfiles.add(sfile) 243 referenced_features[feature] = sfiles 244 245 246def get_features_in_line(line): 247 """Return mentioned Kconfig features in @line.""" 248 return REGEX_FEATURE.findall(line) 249 250 251def parse_kconfig_file(kfile, defined_features, referenced_features): 252 """Parse @kfile and update feature definitions and references.""" 253 lines = [] 254 skip = False 255 256 with open(kfile, "r") as stream: 257 lines = stream.readlines() 258 259 for i in range(len(lines)): 260 line = lines[i] 261 line = line.strip('\n') 262 line = line.split("#")[0] # ignore comments 263 264 if REGEX_KCONFIG_DEF.match(line): 265 feature_def = REGEX_KCONFIG_DEF.findall(line) 266 defined_features.add(feature_def[0]) 267 skip = False 268 elif REGEX_KCONFIG_HELP.match(line): 269 skip = True 270 elif skip: 271 # ignore content of help messages 272 pass 273 elif REGEX_KCONFIG_STMT.match(line): 274 features = get_features_in_line(line) 275 # multi-line statements 276 while line.endswith("\\"): 277 i += 1 278 line = lines[i] 279 line = line.strip('\n') 280 features.extend(get_features_in_line(line)) 281 for feature in set(features): 282 paths = referenced_features.get(feature, set()) 283 paths.add(kfile) 284 referenced_features[feature] = paths 285 286 287if __name__ == "__main__": 288 main() 289