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" % (feature, ", ".join(files)) 140 if opts.find: 141 commits = find_commits(feature, opts.diff) 142 print 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" % (feature, ", ".join(files)) 149 if opts.find: 150 commits = find_commits(feature, opts.diff) 151 print 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" % (feature, ", ".join(files)) 162 163 164def execute(cmd): 165 """Execute %cmd and return stdout. Exit in case of error.""" 166 pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 167 (stdout, _) = pop.communicate() # wait until finished 168 if pop.returncode != 0: 169 sys.exit(stdout) 170 return stdout 171 172 173def find_commits(symbol, diff): 174 """Find commits changing %symbol in the given range of %diff.""" 175 commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" 176 % (symbol, diff)) 177 return commits 178 179 180def tree_is_dirty(): 181 """Return true if the current working tree is dirty (i.e., if any file has 182 been added, deleted, modified, renamed or copied but not committed).""" 183 stdout = execute("git status --porcelain") 184 for line in stdout: 185 if re.findall(r"[URMADC]{1}", line[:2]): 186 return True 187 return False 188 189 190def get_head(): 191 """Return commit hash of current HEAD.""" 192 stdout = execute("git rev-parse HEAD") 193 return stdout.strip('\n') 194 195 196def check_symbols(ignore): 197 """Find undefined Kconfig symbols and return a dict with the symbol as key 198 and a list of referencing files as value. Files matching %ignore are not 199 checked for undefined symbols.""" 200 source_files = [] 201 kconfig_files = [] 202 defined_features = set() 203 referenced_features = dict() # {feature: [files]} 204 205 # use 'git ls-files' to get the worklist 206 stdout = execute("git ls-files") 207 if len(stdout) > 0 and stdout[-1] == "\n": 208 stdout = stdout[:-1] 209 210 for gitfile in stdout.rsplit("\n"): 211 if ".git" in gitfile or "ChangeLog" in gitfile or \ 212 ".log" in gitfile or os.path.isdir(gitfile) or \ 213 gitfile.startswith("tools/"): 214 continue 215 if REGEX_FILE_KCONFIG.match(gitfile): 216 kconfig_files.append(gitfile) 217 else: 218 # all non-Kconfig files are checked for consistency 219 source_files.append(gitfile) 220 221 for sfile in source_files: 222 if ignore and re.match(ignore, sfile): 223 # do not check files matching %ignore 224 continue 225 parse_source_file(sfile, referenced_features) 226 227 for kfile in kconfig_files: 228 if ignore and re.match(ignore, kfile): 229 # do not collect references for files matching %ignore 230 parse_kconfig_file(kfile, defined_features, dict()) 231 else: 232 parse_kconfig_file(kfile, defined_features, referenced_features) 233 234 undefined = {} # {feature: [files]} 235 for feature in sorted(referenced_features): 236 # filter some false positives 237 if feature == "FOO" or feature == "BAR" or \ 238 feature == "FOO_BAR" or feature == "XXX": 239 continue 240 if feature not in defined_features: 241 if feature.endswith("_MODULE"): 242 # avoid false positives for kernel modules 243 if feature[:-len("_MODULE")] in defined_features: 244 continue 245 undefined[feature] = referenced_features.get(feature) 246 return undefined 247 248 249def parse_source_file(sfile, referenced_features): 250 """Parse @sfile for referenced Kconfig features.""" 251 lines = [] 252 with open(sfile, "r") as stream: 253 lines = stream.readlines() 254 255 for line in lines: 256 if not "CONFIG_" in line: 257 continue 258 features = REGEX_SOURCE_FEATURE.findall(line) 259 for feature in features: 260 if not REGEX_FILTER_FEATURES.search(feature): 261 continue 262 sfiles = referenced_features.get(feature, set()) 263 sfiles.add(sfile) 264 referenced_features[feature] = sfiles 265 266 267def get_features_in_line(line): 268 """Return mentioned Kconfig features in @line.""" 269 return REGEX_FEATURE.findall(line) 270 271 272def parse_kconfig_file(kfile, defined_features, referenced_features): 273 """Parse @kfile and update feature definitions and references.""" 274 lines = [] 275 skip = False 276 277 with open(kfile, "r") as stream: 278 lines = stream.readlines() 279 280 for i in range(len(lines)): 281 line = lines[i] 282 line = line.strip('\n') 283 line = line.split("#")[0] # ignore comments 284 285 if REGEX_KCONFIG_DEF.match(line): 286 feature_def = REGEX_KCONFIG_DEF.findall(line) 287 defined_features.add(feature_def[0]) 288 skip = False 289 elif REGEX_KCONFIG_HELP.match(line): 290 skip = True 291 elif skip: 292 # ignore content of help messages 293 pass 294 elif REGEX_KCONFIG_STMT.match(line): 295 features = get_features_in_line(line) 296 # multi-line statements 297 while line.endswith("\\"): 298 i += 1 299 line = lines[i] 300 line = line.strip('\n') 301 features.extend(get_features_in_line(line)) 302 for feature in set(features): 303 paths = referenced_features.get(feature, set()) 304 paths.add(kfile) 305 referenced_features[feature] = paths 306 307 308if __name__ == "__main__": 309 main() 310