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