1#!/usr/bin/env python2 2 3"""Find Kconfig symbols that are referenced but not defined.""" 4 5# (c) 2014-2015 Valentin Rothberg <[email protected]> 6# (c) 2014 Stefan Hengelein <[email protected]> 7# 8# Licensed under the terms of the GNU GPL License version 2 9 10 11import difflib 12import os 13import re 14import signal 15import sys 16from multiprocessing import Pool, cpu_count 17from optparse import OptionParser 18from subprocess import Popen, PIPE, STDOUT 19 20 21# regex expressions 22OPERATORS = r"&|\(|\)|\||\!" 23FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}" 24DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*" 25EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+" 26DEFAULT = r"default\s+.*?(?:if\s.+){,1}" 27STMT = r"^\s*(?:if|select|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR 28SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")" 29 30# regex objects 31REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") 32REGEX_FEATURE = re.compile(r'(?!\B)' + FEATURE + r'(?!\B)') 33REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE) 34REGEX_KCONFIG_DEF = re.compile(DEF) 35REGEX_KCONFIG_EXPR = re.compile(EXPR) 36REGEX_KCONFIG_STMT = re.compile(STMT) 37REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$") 38REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$") 39REGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+") 40REGEX_QUOTES = re.compile("(\"(.*?)\")") 41 42 43def parse_options(): 44 """The user interface of this module.""" 45 usage = "%prog [options]\n\n" \ 46 "Run this tool to detect Kconfig symbols that are referenced but " \ 47 "not defined in\nKconfig. The output of this tool has the " \ 48 "format \'Undefined symbol\\tFile list\'\n\n" \ 49 "If no option is specified, %prog will default to check your\n" \ 50 "current tree. Please note that specifying commits will " \ 51 "\'git reset --hard\'\nyour current tree! You may save " \ 52 "uncommitted changes to avoid losing data." 53 54 parser = OptionParser(usage=usage) 55 56 parser.add_option('-c', '--commit', dest='commit', action='store', 57 default="", 58 help="Check if the specified commit (hash) introduces " 59 "undefined Kconfig symbols.") 60 61 parser.add_option('-d', '--diff', dest='diff', action='store', 62 default="", 63 help="Diff undefined symbols between two commits. The " 64 "input format bases on Git log's " 65 "\'commmit1..commit2\'.") 66 67 parser.add_option('-f', '--find', dest='find', action='store_true', 68 default=False, 69 help="Find and show commits that may cause symbols to be " 70 "missing. Required to run with --diff.") 71 72 parser.add_option('-i', '--ignore', dest='ignore', action='store', 73 default="", 74 help="Ignore files matching this pattern. Note that " 75 "the pattern needs to be a Python regex. To " 76 "ignore defconfigs, specify -i '.*defconfig'.") 77 78 parser.add_option('-s', '--sim', dest='sim', action='store', default="", 79 help="Print a list of maximum 10 string-similar symbols.") 80 81 parser.add_option('', '--force', dest='force', action='store_true', 82 default=False, 83 help="Reset current Git tree even when it's dirty.") 84 85 parser.add_option('', '--no-color', dest='color', action='store_false', 86 default=True, 87 help="Don't print colored output. Default when not " 88 "outputting to a terminal.") 89 90 (opts, _) = parser.parse_args() 91 92 if opts.commit and opts.diff: 93 sys.exit("Please specify only one option at once.") 94 95 if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): 96 sys.exit("Please specify valid input in the following format: " 97 "\'commit1..commit2\'") 98 99 if opts.commit or opts.diff: 100 if not opts.force and tree_is_dirty(): 101 sys.exit("The current Git tree is dirty (see 'git status'). " 102 "Running this script may\ndelete important data since it " 103 "calls 'git reset --hard' for some performance\nreasons. " 104 " Please run this script in a clean Git tree or pass " 105 "'--force' if you\nwant to ignore this warning and " 106 "continue.") 107 108 if opts.commit: 109 opts.find = False 110 111 if opts.ignore: 112 try: 113 re.match(opts.ignore, "this/is/just/a/test.c") 114 except: 115 sys.exit("Please specify a valid Python regex.") 116 117 return opts 118 119 120def main(): 121 """Main function of this module.""" 122 opts = parse_options() 123 124 global color 125 color = opts.color and sys.stdout.isatty() 126 127 if opts.sim and not opts.commit and not opts.diff: 128 sims = find_sims(opts.sim, opts.ignore) 129 if sims: 130 print "%s: %s" % (yel("Similar symbols"), ', '.join(sims)) 131 else: 132 print "%s: no similar symbols found" % yel("Similar symbols") 133 sys.exit(0) 134 135 # dictionary of (un)defined symbols 136 defined = {} 137 undefined = {} 138 139 if opts.commit or opts.diff: 140 head = get_head() 141 142 # get commit range 143 commit_a = None 144 commit_b = None 145 if opts.commit: 146 commit_a = opts.commit + "~" 147 commit_b = opts.commit 148 elif opts.diff: 149 split = opts.diff.split("..") 150 commit_a = split[0] 151 commit_b = split[1] 152 undefined_a = {} 153 undefined_b = {} 154 155 # get undefined items before the commit 156 execute("git reset --hard %s" % commit_a) 157 undefined_a, _ = check_symbols(opts.ignore) 158 159 # get undefined items for the commit 160 execute("git reset --hard %s" % commit_b) 161 undefined_b, defined = check_symbols(opts.ignore) 162 163 # report cases that are present for the commit but not before 164 for feature in sorted(undefined_b): 165 # feature has not been undefined before 166 if not feature in undefined_a: 167 files = sorted(undefined_b.get(feature)) 168 undefined[feature] = files 169 # check if there are new files that reference the undefined feature 170 else: 171 files = sorted(undefined_b.get(feature) - 172 undefined_a.get(feature)) 173 if files: 174 undefined[feature] = files 175 176 # reset to head 177 execute("git reset --hard %s" % head) 178 179 # default to check the entire tree 180 else: 181 undefined, defined = check_symbols(opts.ignore) 182 183 # now print the output 184 for feature in sorted(undefined): 185 print red(feature) 186 187 files = sorted(undefined.get(feature)) 188 print "%s: %s" % (yel("Referencing files"), ", ".join(files)) 189 190 sims = find_sims(feature, opts.ignore, defined) 191 sims_out = yel("Similar symbols") 192 if sims: 193 print "%s: %s" % (sims_out, ', '.join(sims)) 194 else: 195 print "%s: %s" % (sims_out, "no similar symbols found") 196 197 if opts.find: 198 print "%s:" % yel("Commits changing symbol") 199 commits = find_commits(feature, opts.diff) 200 if commits: 201 for commit in commits: 202 commit = commit.split(" ", 1) 203 print "\t- %s (\"%s\")" % (yel(commit[0]), commit[1]) 204 else: 205 print "\t- no commit found" 206 print # new line 207 208 209def yel(string): 210 """ 211 Color %string yellow. 212 """ 213 return "\033[33m%s\033[0m" % string if color else string 214 215 216def red(string): 217 """ 218 Color %string red. 219 """ 220 return "\033[31m%s\033[0m" % string if color else string 221 222 223def execute(cmd): 224 """Execute %cmd and return stdout. Exit in case of error.""" 225 pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 226 (stdout, _) = pop.communicate() # wait until finished 227 if pop.returncode != 0: 228 sys.exit(stdout) 229 return stdout 230 231 232def find_commits(symbol, diff): 233 """Find commits changing %symbol in the given range of %diff.""" 234 commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" 235 % (symbol, diff)) 236 return [x for x in commits.split("\n") if x] 237 238 239def tree_is_dirty(): 240 """Return true if the current working tree is dirty (i.e., if any file has 241 been added, deleted, modified, renamed or copied but not committed).""" 242 stdout = execute("git status --porcelain") 243 for line in stdout: 244 if re.findall(r"[URMADC]{1}", line[:2]): 245 return True 246 return False 247 248 249def get_head(): 250 """Return commit hash of current HEAD.""" 251 stdout = execute("git rev-parse HEAD") 252 return stdout.strip('\n') 253 254 255def partition(lst, size): 256 """Partition list @lst into eveni-sized lists of size @size.""" 257 return [lst[i::size] for i in xrange(size)] 258 259 260def init_worker(): 261 """Set signal handler to ignore SIGINT.""" 262 signal.signal(signal.SIGINT, signal.SIG_IGN) 263 264 265def find_sims(symbol, ignore, defined = []): 266 """Return a list of max. ten Kconfig symbols that are string-similar to 267 @symbol.""" 268 if defined: 269 return sorted(difflib.get_close_matches(symbol, set(defined), 10)) 270 271 pool = Pool(cpu_count(), init_worker) 272 kfiles = [] 273 for gitfile in get_files(): 274 if REGEX_FILE_KCONFIG.match(gitfile): 275 kfiles.append(gitfile) 276 277 arglist = [] 278 for part in partition(kfiles, cpu_count()): 279 arglist.append((part, ignore)) 280 281 for res in pool.map(parse_kconfig_files, arglist): 282 defined.extend(res[0]) 283 284 return sorted(difflib.get_close_matches(symbol, set(defined), 10)) 285 286 287def get_files(): 288 """Return a list of all files in the current git directory.""" 289 # use 'git ls-files' to get the worklist 290 stdout = execute("git ls-files") 291 if len(stdout) > 0 and stdout[-1] == "\n": 292 stdout = stdout[:-1] 293 294 files = [] 295 for gitfile in stdout.rsplit("\n"): 296 if ".git" in gitfile or "ChangeLog" in gitfile or \ 297 ".log" in gitfile or os.path.isdir(gitfile) or \ 298 gitfile.startswith("tools/"): 299 continue 300 files.append(gitfile) 301 return files 302 303 304def check_symbols(ignore): 305 """Find undefined Kconfig symbols and return a dict with the symbol as key 306 and a list of referencing files as value. Files matching %ignore are not 307 checked for undefined symbols.""" 308 pool = Pool(cpu_count(), init_worker) 309 try: 310 return check_symbols_helper(pool, ignore) 311 except KeyboardInterrupt: 312 pool.terminate() 313 pool.join() 314 sys.exit(1) 315 316 317def check_symbols_helper(pool, ignore): 318 """Helper method for check_symbols(). Used to catch keyboard interrupts in 319 check_symbols() in order to properly terminate running worker processes.""" 320 source_files = [] 321 kconfig_files = [] 322 defined_features = [] 323 referenced_features = dict() # {file: [features]} 324 325 for gitfile in get_files(): 326 if REGEX_FILE_KCONFIG.match(gitfile): 327 kconfig_files.append(gitfile) 328 else: 329 if ignore and not re.match(ignore, gitfile): 330 continue 331 # add source files that do not match the ignore pattern 332 source_files.append(gitfile) 333 334 # parse source files 335 arglist = partition(source_files, cpu_count()) 336 for res in pool.map(parse_source_files, arglist): 337 referenced_features.update(res) 338 339 340 # parse kconfig files 341 arglist = [] 342 for part in partition(kconfig_files, cpu_count()): 343 arglist.append((part, ignore)) 344 for res in pool.map(parse_kconfig_files, arglist): 345 defined_features.extend(res[0]) 346 referenced_features.update(res[1]) 347 defined_features = set(defined_features) 348 349 # inverse mapping of referenced_features to dict(feature: [files]) 350 inv_map = dict() 351 for _file, features in referenced_features.iteritems(): 352 for feature in features: 353 inv_map[feature] = inv_map.get(feature, set()) 354 inv_map[feature].add(_file) 355 referenced_features = inv_map 356 357 undefined = {} # {feature: [files]} 358 for feature in sorted(referenced_features): 359 # filter some false positives 360 if feature == "FOO" or feature == "BAR" or \ 361 feature == "FOO_BAR" or feature == "XXX": 362 continue 363 if feature not in defined_features: 364 if feature.endswith("_MODULE"): 365 # avoid false positives for kernel modules 366 if feature[:-len("_MODULE")] in defined_features: 367 continue 368 undefined[feature] = referenced_features.get(feature) 369 return undefined, defined_features 370 371 372def parse_source_files(source_files): 373 """Parse each source file in @source_files and return dictionary with source 374 files as keys and lists of references Kconfig symbols as values.""" 375 referenced_features = dict() 376 for sfile in source_files: 377 referenced_features[sfile] = parse_source_file(sfile) 378 return referenced_features 379 380 381def parse_source_file(sfile): 382 """Parse @sfile and return a list of referenced Kconfig features.""" 383 lines = [] 384 references = [] 385 386 if not os.path.exists(sfile): 387 return references 388 389 with open(sfile, "r") as stream: 390 lines = stream.readlines() 391 392 for line in lines: 393 if not "CONFIG_" in line: 394 continue 395 features = REGEX_SOURCE_FEATURE.findall(line) 396 for feature in features: 397 if not REGEX_FILTER_FEATURES.search(feature): 398 continue 399 references.append(feature) 400 401 return references 402 403 404def get_features_in_line(line): 405 """Return mentioned Kconfig features in @line.""" 406 return REGEX_FEATURE.findall(line) 407 408 409def parse_kconfig_files(args): 410 """Parse kconfig files and return tuple of defined and references Kconfig 411 symbols. Note, @args is a tuple of a list of files and the @ignore 412 pattern.""" 413 kconfig_files = args[0] 414 ignore = args[1] 415 defined_features = [] 416 referenced_features = dict() 417 418 for kfile in kconfig_files: 419 defined, references = parse_kconfig_file(kfile) 420 defined_features.extend(defined) 421 if ignore and re.match(ignore, kfile): 422 # do not collect references for files that match the ignore pattern 423 continue 424 referenced_features[kfile] = references 425 return (defined_features, referenced_features) 426 427 428def parse_kconfig_file(kfile): 429 """Parse @kfile and update feature definitions and references.""" 430 lines = [] 431 defined = [] 432 references = [] 433 skip = False 434 435 if not os.path.exists(kfile): 436 return defined, references 437 438 with open(kfile, "r") as stream: 439 lines = stream.readlines() 440 441 for i in range(len(lines)): 442 line = lines[i] 443 line = line.strip('\n') 444 line = line.split("#")[0] # ignore comments 445 446 if REGEX_KCONFIG_DEF.match(line): 447 feature_def = REGEX_KCONFIG_DEF.findall(line) 448 defined.append(feature_def[0]) 449 skip = False 450 elif REGEX_KCONFIG_HELP.match(line): 451 skip = True 452 elif skip: 453 # ignore content of help messages 454 pass 455 elif REGEX_KCONFIG_STMT.match(line): 456 line = REGEX_QUOTES.sub("", line) 457 features = get_features_in_line(line) 458 # multi-line statements 459 while line.endswith("\\"): 460 i += 1 461 line = lines[i] 462 line = line.strip('\n') 463 features.extend(get_features_in_line(line)) 464 for feature in set(features): 465 if REGEX_NUMERIC.match(feature): 466 # ignore numeric values 467 continue 468 references.append(feature) 469 470 return defined, references 471 472 473if __name__ == "__main__": 474 main() 475