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