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 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('-f', '--find', dest='find', action='store_true', 62 default=False, 63 help="Find and show commits that may cause symbols to be " 64 "missing. Required to run with --diff.") 65 66 parser.add_option('-i', '--ignore', dest='ignore', action='store', 67 default="", 68 help="Ignore files matching this pattern. Note that " 69 "the pattern needs to be a Python regex. To " 70 "ignore defconfigs, specify -i '.*defconfig'.") 71 72 parser.add_option('', '--force', dest='force', action='store_true', 73 default=False, 74 help="Reset current Git tree even when it's dirty.") 75 76 (opts, _) = parser.parse_args() 77 78 if opts.commit and opts.diff: 79 sys.exit("Please specify only one option at once.") 80 81 if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): 82 sys.exit("Please specify valid input in the following format: " 83 "\'commmit1..commit2\'") 84 85 if opts.commit or opts.diff: 86 if not opts.force and tree_is_dirty(): 87 sys.exit("The current Git tree is dirty (see 'git status'). " 88 "Running this script may\ndelete important data since it " 89 "calls 'git reset --hard' for some performance\nreasons. " 90 " Please run this script in a clean Git tree or pass " 91 "'--force' if you\nwant to ignore this warning and " 92 "continue.") 93 94 if opts.commit: 95 opts.find = False 96 97 if opts.ignore: 98 try: 99 re.match(opts.ignore, "this/is/just/a/test.c") 100 except: 101 sys.exit("Please specify a valid Python regex.") 102 103 return opts 104 105 106def main(): 107 """Main function of this module.""" 108 opts = parse_options() 109 110 if opts.commit or opts.diff: 111 head = get_head() 112 113 # get commit range 114 commit_a = None 115 commit_b = None 116 if opts.commit: 117 commit_a = opts.commit + "~" 118 commit_b = opts.commit 119 elif opts.diff: 120 split = opts.diff.split("..") 121 commit_a = split[0] 122 commit_b = split[1] 123 undefined_a = {} 124 undefined_b = {} 125 126 # get undefined items before the commit 127 execute("git reset --hard %s" % commit_a) 128 undefined_a = check_symbols(opts.ignore) 129 130 # get undefined items for the commit 131 execute("git reset --hard %s" % commit_b) 132 undefined_b = check_symbols(opts.ignore) 133 134 # report cases that are present for the commit but not before 135 for feature in sorted(undefined_b): 136 # feature has not been undefined before 137 if not feature in undefined_a: 138 files = sorted(undefined_b.get(feature)) 139 print "%s\t%s" % (yel(feature), ", ".join(files)) 140 if opts.find: 141 commits = find_commits(feature, opts.diff) 142 print red(commits) 143 # check if there are new files that reference the undefined feature 144 else: 145 files = sorted(undefined_b.get(feature) - 146 undefined_a.get(feature)) 147 if files: 148 print "%s\t%s" % (yel(feature), ", ".join(files)) 149 if opts.find: 150 commits = find_commits(feature, opts.diff) 151 print red(commits) 152 153 # reset to head 154 execute("git reset --hard %s" % head) 155 156 # default to check the entire tree 157 else: 158 undefined = check_symbols(opts.ignore) 159 for feature in sorted(undefined): 160 files = sorted(undefined.get(feature)) 161 print "%s\t%s" % (yel(feature), ", ".join(files)) 162 163 164def yel(string): 165 """ 166 Color %string yellow. 167 """ 168 return "\033[33m%s\033[0m" % string 169 170 171def red(string): 172 """ 173 Color %string red. 174 """ 175 return "\033[31m%s\033[0m" % string 176 177 178def execute(cmd): 179 """Execute %cmd and return stdout. Exit in case of error.""" 180 pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 181 (stdout, _) = pop.communicate() # wait until finished 182 if pop.returncode != 0: 183 sys.exit(stdout) 184 return stdout 185 186 187def find_commits(symbol, diff): 188 """Find commits changing %symbol in the given range of %diff.""" 189 commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" 190 % (symbol, diff)) 191 return commits 192 193 194def tree_is_dirty(): 195 """Return true if the current working tree is dirty (i.e., if any file has 196 been added, deleted, modified, renamed or copied but not committed).""" 197 stdout = execute("git status --porcelain") 198 for line in stdout: 199 if re.findall(r"[URMADC]{1}", line[:2]): 200 return True 201 return False 202 203 204def get_head(): 205 """Return commit hash of current HEAD.""" 206 stdout = execute("git rev-parse HEAD") 207 return stdout.strip('\n') 208 209 210def check_symbols(ignore): 211 """Find undefined Kconfig symbols and return a dict with the symbol as key 212 and a list of referencing files as value. Files matching %ignore are not 213 checked for undefined symbols.""" 214 source_files = [] 215 kconfig_files = [] 216 defined_features = set() 217 referenced_features = dict() # {feature: [files]} 218 219 # use 'git ls-files' to get the worklist 220 stdout = execute("git ls-files") 221 if len(stdout) > 0 and stdout[-1] == "\n": 222 stdout = stdout[:-1] 223 224 for gitfile in stdout.rsplit("\n"): 225 if ".git" in gitfile or "ChangeLog" in gitfile or \ 226 ".log" in gitfile or os.path.isdir(gitfile) or \ 227 gitfile.startswith("tools/"): 228 continue 229 if REGEX_FILE_KCONFIG.match(gitfile): 230 kconfig_files.append(gitfile) 231 else: 232 # all non-Kconfig files are checked for consistency 233 source_files.append(gitfile) 234 235 for sfile in source_files: 236 if ignore and re.match(ignore, sfile): 237 # do not check files matching %ignore 238 continue 239 parse_source_file(sfile, referenced_features) 240 241 for kfile in kconfig_files: 242 if ignore and re.match(ignore, kfile): 243 # do not collect references for files matching %ignore 244 parse_kconfig_file(kfile, defined_features, dict()) 245 else: 246 parse_kconfig_file(kfile, defined_features, referenced_features) 247 248 undefined = {} # {feature: [files]} 249 for feature in sorted(referenced_features): 250 # filter some false positives 251 if feature == "FOO" or feature == "BAR" or \ 252 feature == "FOO_BAR" or feature == "XXX": 253 continue 254 if feature not in defined_features: 255 if feature.endswith("_MODULE"): 256 # avoid false positives for kernel modules 257 if feature[:-len("_MODULE")] in defined_features: 258 continue 259 undefined[feature] = referenced_features.get(feature) 260 return undefined 261 262 263def parse_source_file(sfile, referenced_features): 264 """Parse @sfile for referenced Kconfig features.""" 265 lines = [] 266 with open(sfile, "r") as stream: 267 lines = stream.readlines() 268 269 for line in lines: 270 if not "CONFIG_" in line: 271 continue 272 features = REGEX_SOURCE_FEATURE.findall(line) 273 for feature in features: 274 if not REGEX_FILTER_FEATURES.search(feature): 275 continue 276 sfiles = referenced_features.get(feature, set()) 277 sfiles.add(sfile) 278 referenced_features[feature] = sfiles 279 280 281def get_features_in_line(line): 282 """Return mentioned Kconfig features in @line.""" 283 return REGEX_FEATURE.findall(line) 284 285 286def parse_kconfig_file(kfile, defined_features, referenced_features): 287 """Parse @kfile and update feature definitions and references.""" 288 lines = [] 289 skip = False 290 291 with open(kfile, "r") as stream: 292 lines = stream.readlines() 293 294 for i in range(len(lines)): 295 line = lines[i] 296 line = line.strip('\n') 297 line = line.split("#")[0] # ignore comments 298 299 if REGEX_KCONFIG_DEF.match(line): 300 feature_def = REGEX_KCONFIG_DEF.findall(line) 301 defined_features.add(feature_def[0]) 302 skip = False 303 elif REGEX_KCONFIG_HELP.match(line): 304 skip = True 305 elif skip: 306 # ignore content of help messages 307 pass 308 elif REGEX_KCONFIG_STMT.match(line): 309 features = get_features_in_line(line) 310 # multi-line statements 311 while line.endswith("\\"): 312 i += 1 313 line = lines[i] 314 line = line.strip('\n') 315 features.extend(get_features_in_line(line)) 316 for feature in set(features): 317 paths = referenced_features.get(feature, set()) 318 paths.add(kfile) 319 referenced_features[feature] = paths 320 321 322if __name__ == "__main__": 323 main() 324