1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0-only 3 4"""Find Kconfig symbols that are referenced but not defined.""" 5 6# (c) 2014-2017 Valentin Rothberg <[email protected]> 7# (c) 2014 Stefan Hengelein <[email protected]> 8# 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"&|\(|\)|\||\!" 23SYMBOL = r"(?:\w*[A-Z0-9]\w*){2,}" 24DEF = r"^\s*(?:menu){,1}config\s+(" + SYMBOL + r")\s*" 25EXPR = r"(?:" + OPERATORS + r"|\s|" + SYMBOL + r")+" 26DEFAULT = r"default\s+.*?(?:if\s.+){,1}" 27STMT = r"^\s*(?:if|select|imply|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR 28SOURCE_SYMBOL = r"(?:\W|\b)+[D]{,1}CONFIG_(" + SYMBOL + r")" 29 30# regex objects 31REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") 32REGEX_SYMBOL = re.compile(r'(?!\B)' + SYMBOL + r'(?!\B)') 33REGEX_SOURCE_SYMBOL = re.compile(SOURCE_SYMBOL) 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\s*$") 38REGEX_FILTER_SYMBOLS = 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 if args.commit.startswith('HEAD'): 106 sys.exit("The --commit option can't use the HEAD ref") 107 108 args.find = False 109 110 if args.ignore: 111 try: 112 re.match(args.ignore, "this/is/just/a/test.c") 113 except: 114 sys.exit("Please specify a valid Python regex.") 115 116 return args 117 118 119def main(): 120 """Main function of this module.""" 121 args = parse_options() 122 123 global COLOR 124 COLOR = args.color and sys.stdout.isatty() 125 126 if args.sim and not args.commit and not args.diff: 127 sims = find_sims(args.sim, args.ignore) 128 if sims: 129 print("%s: %s" % (yel("Similar symbols"), ', '.join(sims))) 130 else: 131 print("%s: no similar symbols found" % yel("Similar symbols")) 132 sys.exit(0) 133 134 # dictionary of (un)defined symbols 135 defined = {} 136 undefined = {} 137 138 if args.commit or args.diff: 139 head = get_head() 140 141 # get commit range 142 commit_a = None 143 commit_b = None 144 if args.commit: 145 commit_a = args.commit + "~" 146 commit_b = args.commit 147 elif args.diff: 148 split = args.diff.split("..") 149 commit_a = split[0] 150 commit_b = split[1] 151 undefined_a = {} 152 undefined_b = {} 153 154 # get undefined items before the commit 155 reset(commit_a) 156 undefined_a, _ = check_symbols(args.ignore) 157 158 # get undefined items for the commit 159 reset(commit_b) 160 undefined_b, defined = check_symbols(args.ignore) 161 162 # report cases that are present for the commit but not before 163 for symbol in sorted(undefined_b): 164 # symbol has not been undefined before 165 if symbol not in undefined_a: 166 files = sorted(undefined_b.get(symbol)) 167 undefined[symbol] = files 168 # check if there are new files that reference the undefined symbol 169 else: 170 files = sorted(undefined_b.get(symbol) - 171 undefined_a.get(symbol)) 172 if files: 173 undefined[symbol] = files 174 175 # reset to head 176 reset(head) 177 178 # default to check the entire tree 179 else: 180 undefined, defined = check_symbols(args.ignore) 181 182 # now print the output 183 for symbol in sorted(undefined): 184 print(red(symbol)) 185 186 files = sorted(undefined.get(symbol)) 187 print("%s: %s" % (yel("Referencing files"), ", ".join(files))) 188 189 sims = find_sims(symbol, args.ignore, defined) 190 sims_out = yel("Similar symbols") 191 if sims: 192 print("%s: %s" % (sims_out, ', '.join(sims))) 193 else: 194 print("%s: %s" % (sims_out, "no similar symbols found")) 195 196 if args.find: 197 print("%s:" % yel("Commits changing symbol")) 198 commits = find_commits(symbol, args.diff) 199 if commits: 200 for commit in commits: 201 commit = commit.split(" ", 1) 202 print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1])) 203 else: 204 print("\t- no commit found") 205 print() # new line 206 207 208def reset(commit): 209 """Reset current git tree to %commit.""" 210 execute(["git", "reset", "--hard", commit]) 211 212 213def yel(string): 214 """ 215 Color %string yellow. 216 """ 217 return "\033[33m%s\033[0m" % string if COLOR else string 218 219 220def red(string): 221 """ 222 Color %string red. 223 """ 224 return "\033[31m%s\033[0m" % string if COLOR else string 225 226 227def execute(cmd): 228 """Execute %cmd and return stdout. Exit in case of error.""" 229 try: 230 stdout = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=False) 231 stdout = stdout.decode(errors='replace') 232 except subprocess.CalledProcessError as fail: 233 exit(fail) 234 return stdout 235 236 237def find_commits(symbol, diff): 238 """Find commits changing %symbol in the given range of %diff.""" 239 commits = execute(["git", "log", "--pretty=oneline", 240 "--abbrev-commit", "-G", 241 symbol, diff]) 242 return [x for x in commits.split("\n") if x] 243 244 245def tree_is_dirty(): 246 """Return true if the current working tree is dirty (i.e., if any file has 247 been added, deleted, modified, renamed or copied but not committed).""" 248 stdout = execute(["git", "status", "--porcelain"]) 249 for line in stdout: 250 if re.findall(r"[URMADC]{1}", line[:2]): 251 return True 252 return False 253 254 255def get_head(): 256 """Return commit hash of current HEAD.""" 257 stdout = execute(["git", "rev-parse", "HEAD"]) 258 return stdout.strip('\n') 259 260 261def partition(lst, size): 262 """Partition list @lst into eveni-sized lists of size @size.""" 263 return [lst[i::size] for i in range(size)] 264 265 266def init_worker(): 267 """Set signal handler to ignore SIGINT.""" 268 signal.signal(signal.SIGINT, signal.SIG_IGN) 269 270 271def find_sims(symbol, ignore, defined=[]): 272 """Return a list of max. ten Kconfig symbols that are string-similar to 273 @symbol.""" 274 if defined: 275 return difflib.get_close_matches(symbol, set(defined), 10) 276 277 pool = Pool(cpu_count(), init_worker) 278 kfiles = [] 279 for gitfile in get_files(): 280 if REGEX_FILE_KCONFIG.match(gitfile): 281 kfiles.append(gitfile) 282 283 arglist = [] 284 for part in partition(kfiles, cpu_count()): 285 arglist.append((part, ignore)) 286 287 for res in pool.map(parse_kconfig_files, arglist): 288 defined.extend(res[0]) 289 290 return difflib.get_close_matches(symbol, set(defined), 10) 291 292 293def get_files(): 294 """Return a list of all files in the current git directory.""" 295 # use 'git ls-files' to get the worklist 296 stdout = execute(["git", "ls-files"]) 297 if len(stdout) > 0 and stdout[-1] == "\n": 298 stdout = stdout[:-1] 299 300 files = [] 301 for gitfile in stdout.rsplit("\n"): 302 if ".git" in gitfile or "ChangeLog" in gitfile or \ 303 ".log" in gitfile or os.path.isdir(gitfile) or \ 304 gitfile.startswith("tools/"): 305 continue 306 files.append(gitfile) 307 return files 308 309 310def check_symbols(ignore): 311 """Find undefined Kconfig symbols and return a dict with the symbol as key 312 and a list of referencing files as value. Files matching %ignore are not 313 checked for undefined symbols.""" 314 pool = Pool(cpu_count(), init_worker) 315 try: 316 return check_symbols_helper(pool, ignore) 317 except KeyboardInterrupt: 318 pool.terminate() 319 pool.join() 320 sys.exit(1) 321 322 323def check_symbols_helper(pool, ignore): 324 """Helper method for check_symbols(). Used to catch keyboard interrupts in 325 check_symbols() in order to properly terminate running worker processes.""" 326 source_files = [] 327 kconfig_files = [] 328 defined_symbols = [] 329 referenced_symbols = dict() # {file: [symbols]} 330 331 for gitfile in get_files(): 332 if REGEX_FILE_KCONFIG.match(gitfile): 333 kconfig_files.append(gitfile) 334 else: 335 if ignore and re.match(ignore, gitfile): 336 continue 337 # add source files that do not match the ignore pattern 338 source_files.append(gitfile) 339 340 # parse source files 341 arglist = partition(source_files, cpu_count()) 342 for res in pool.map(parse_source_files, arglist): 343 referenced_symbols.update(res) 344 345 # parse kconfig files 346 arglist = [] 347 for part in partition(kconfig_files, cpu_count()): 348 arglist.append((part, ignore)) 349 for res in pool.map(parse_kconfig_files, arglist): 350 defined_symbols.extend(res[0]) 351 referenced_symbols.update(res[1]) 352 defined_symbols = set(defined_symbols) 353 354 # inverse mapping of referenced_symbols to dict(symbol: [files]) 355 inv_map = dict() 356 for _file, symbols in referenced_symbols.items(): 357 for symbol in symbols: 358 inv_map[symbol] = inv_map.get(symbol, set()) 359 inv_map[symbol].add(_file) 360 referenced_symbols = inv_map 361 362 undefined = {} # {symbol: [files]} 363 for symbol in sorted(referenced_symbols): 364 # filter some false positives 365 if symbol == "FOO" or symbol == "BAR" or \ 366 symbol == "FOO_BAR" or symbol == "XXX": 367 continue 368 if symbol not in defined_symbols: 369 if symbol.endswith("_MODULE"): 370 # avoid false positives for kernel modules 371 if symbol[:-len("_MODULE")] in defined_symbols: 372 continue 373 undefined[symbol] = referenced_symbols.get(symbol) 374 return undefined, defined_symbols 375 376 377def parse_source_files(source_files): 378 """Parse each source file in @source_files and return dictionary with source 379 files as keys and lists of references Kconfig symbols as values.""" 380 referenced_symbols = dict() 381 for sfile in source_files: 382 referenced_symbols[sfile] = parse_source_file(sfile) 383 return referenced_symbols 384 385 386def parse_source_file(sfile): 387 """Parse @sfile and return a list of referenced Kconfig symbols.""" 388 lines = [] 389 references = [] 390 391 if not os.path.exists(sfile): 392 return references 393 394 with open(sfile, "r", encoding='utf-8', errors='replace') as stream: 395 lines = stream.readlines() 396 397 for line in lines: 398 if "CONFIG_" not in line: 399 continue 400 symbols = REGEX_SOURCE_SYMBOL.findall(line) 401 for symbol in symbols: 402 if not REGEX_FILTER_SYMBOLS.search(symbol): 403 continue 404 references.append(symbol) 405 406 return references 407 408 409def get_symbols_in_line(line): 410 """Return mentioned Kconfig symbols in @line.""" 411 return REGEX_SYMBOL.findall(line) 412 413 414def parse_kconfig_files(args): 415 """Parse kconfig files and return tuple of defined and references Kconfig 416 symbols. Note, @args is a tuple of a list of files and the @ignore 417 pattern.""" 418 kconfig_files = args[0] 419 ignore = args[1] 420 defined_symbols = [] 421 referenced_symbols = dict() 422 423 for kfile in kconfig_files: 424 defined, references = parse_kconfig_file(kfile) 425 defined_symbols.extend(defined) 426 if ignore and re.match(ignore, kfile): 427 # do not collect references for files that match the ignore pattern 428 continue 429 referenced_symbols[kfile] = references 430 return (defined_symbols, referenced_symbols) 431 432 433def parse_kconfig_file(kfile): 434 """Parse @kfile and update symbol definitions and references.""" 435 lines = [] 436 defined = [] 437 references = [] 438 skip = False 439 440 if not os.path.exists(kfile): 441 return defined, references 442 443 with open(kfile, "r", encoding='utf-8', errors='replace') as stream: 444 lines = stream.readlines() 445 446 for i in range(len(lines)): 447 line = lines[i] 448 line = line.strip('\n') 449 line = line.split("#")[0] # ignore comments 450 451 if REGEX_KCONFIG_DEF.match(line): 452 symbol_def = REGEX_KCONFIG_DEF.findall(line) 453 defined.append(symbol_def[0]) 454 skip = False 455 elif REGEX_KCONFIG_HELP.match(line): 456 skip = True 457 elif skip: 458 # ignore content of help messages 459 pass 460 elif REGEX_KCONFIG_STMT.match(line): 461 line = REGEX_QUOTES.sub("", line) 462 symbols = get_symbols_in_line(line) 463 # multi-line statements 464 while line.endswith("\\"): 465 i += 1 466 line = lines[i] 467 line = line.strip('\n') 468 symbols.extend(get_symbols_in_line(line)) 469 for symbol in set(symbols): 470 if REGEX_NUMERIC.match(symbol): 471 # ignore numeric values 472 continue 473 references.append(symbol) 474 475 return defined, references 476 477 478if __name__ == "__main__": 479 main() 480