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