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