3 """Find Kconfig symbols that are referenced but not defined."""
5 # (c) 2014-2015 Valentin Rothberg <valentinrothberg@gmail.com>
6 # (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
8 # Licensed under the terms of the GNU GPL License version 2
14 from subprocess
import Popen
, PIPE
, STDOUT
15 from optparse
import OptionParser
19 OPERATORS
= r
"&|\(|\)|\||\!"
20 FEATURE
= r
"(?:\w*[A-Z0-9]\w*){2,}"
21 DEF
= r
"^\s*(?:menu){,1}config\s+(" + FEATURE
+ r
")\s*"
22 EXPR
= r
"(?:" + OPERATORS
+ r
"|\s|" + FEATURE
+ r
")+"
23 DEFAULT
= r
"default\s+.*?(?:if\s.+){,1}"
24 STMT
= r
"^\s*(?:if|select|depends\s+on|(?:" + DEFAULT
+ r
"))\s+" + EXPR
25 SOURCE_FEATURE
= r
"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE
+ r
")"
28 REGEX_FILE_KCONFIG
= re
.compile(r
".*Kconfig[\.\w+\-]*$")
29 REGEX_FEATURE
= re
.compile(r
'(?!\B"[^"]*)' + FEATURE
+ r
'(?![^"]*"\B)')
30 REGEX_SOURCE_FEATURE
= re
.compile(SOURCE_FEATURE
)
31 REGEX_KCONFIG_DEF
= re
.compile(DEF
)
32 REGEX_KCONFIG_EXPR
= re
.compile(EXPR
)
33 REGEX_KCONFIG_STMT
= re
.compile(STMT
)
34 REGEX_KCONFIG_HELP
= re
.compile(r
"^\s+(help|---help---)\s*$")
35 REGEX_FILTER_FEATURES
= re
.compile(r
"[A-Za-z0-9]$")
36 REGEX_NUMERIC
= re
.compile(r
"0[xX][0-9a-fA-F]+|[0-9]+")
40 """The user interface of this module."""
41 usage
= "%prog [options]\n\n" \
42 "Run this tool to detect Kconfig symbols that are referenced but " \
43 "not defined in\nKconfig. The output of this tool has the " \
44 "format \'Undefined symbol\\tFile list\'\n\n" \
45 "If no option is specified, %prog will default to check your\n" \
46 "current tree. Please note that specifying commits will " \
47 "\'git reset --hard\'\nyour current tree! You may save " \
48 "uncommitted changes to avoid losing data."
50 parser
= OptionParser(usage
=usage
)
52 parser
.add_option('-c', '--commit', dest
='commit', action
='store',
54 help="Check if the specified commit (hash) introduces "
55 "undefined Kconfig symbols.")
57 parser
.add_option('-d', '--diff', dest
='diff', action
='store',
59 help="Diff undefined symbols between two commits. The "
60 "input format bases on Git log's "
61 "\'commmit1..commit2\'.")
63 parser
.add_option('-f', '--find', dest
='find', action
='store_true',
65 help="Find and show commits that may cause symbols to be "
66 "missing. Required to run with --diff.")
68 parser
.add_option('-i', '--ignore', dest
='ignore', action
='store',
70 help="Ignore files matching this pattern. Note that "
71 "the pattern needs to be a Python regex. To "
72 "ignore defconfigs, specify -i '.*defconfig'.")
74 parser
.add_option('', '--force', dest
='force', action
='store_true',
76 help="Reset current Git tree even when it's dirty.")
78 (opts
, _
) = parser
.parse_args()
80 if opts
.commit
and opts
.diff
:
81 sys
.exit("Please specify only one option at once.")
83 if opts
.diff
and not re
.match(r
"^[\w\-\.]+\.\.[\w\-\.]+$", opts
.diff
):
84 sys
.exit("Please specify valid input in the following format: "
85 "\'commmit1..commit2\'")
87 if opts
.commit
or opts
.diff
:
88 if not opts
.force
and tree_is_dirty():
89 sys
.exit("The current Git tree is dirty (see 'git status'). "
90 "Running this script may\ndelete important data since it "
91 "calls 'git reset --hard' for some performance\nreasons. "
92 " Please run this script in a clean Git tree or pass "
93 "'--force' if you\nwant to ignore this warning and "
101 re
.match(opts
.ignore
, "this/is/just/a/test.c")
103 sys
.exit("Please specify a valid Python regex.")
109 """Main function of this module."""
110 opts
= parse_options()
112 if opts
.commit
or opts
.diff
:
119 commit_a
= opts
.commit
+ "~"
120 commit_b
= opts
.commit
122 split
= opts
.diff
.split("..")
128 # get undefined items before the commit
129 execute("git reset --hard %s" % commit_a
)
130 undefined_a
= check_symbols(opts
.ignore
)
132 # get undefined items for the commit
133 execute("git reset --hard %s" % commit_b
)
134 undefined_b
= check_symbols(opts
.ignore
)
136 # report cases that are present for the commit but not before
137 for feature
in sorted(undefined_b
):
138 # feature has not been undefined before
139 if not feature
in undefined_a
:
140 files
= sorted(undefined_b
.get(feature
))
141 print "%s\t%s" % (yel(feature
), ", ".join(files
))
143 commits
= find_commits(feature
, opts
.diff
)
145 # check if there are new files that reference the undefined feature
147 files
= sorted(undefined_b
.get(feature
) -
148 undefined_a
.get(feature
))
150 print "%s\t%s" % (yel(feature
), ", ".join(files
))
152 commits
= find_commits(feature
, opts
.diff
)
156 execute("git reset --hard %s" % head
)
158 # default to check the entire tree
160 undefined
= check_symbols(opts
.ignore
)
161 for feature
in sorted(undefined
):
162 files
= sorted(undefined
.get(feature
))
163 print "%s\t%s" % (yel(feature
), ", ".join(files
))
168 Color %string yellow.
170 return "\033[33m%s\033[0m" % string
177 return "\033[31m%s\033[0m" % string
181 """Execute %cmd and return stdout. Exit in case of error."""
182 pop
= Popen(cmd
, stdout
=PIPE
, stderr
=STDOUT
, shell
=True)
183 (stdout
, _
) = pop
.communicate() # wait until finished
184 if pop
.returncode
!= 0:
189 def find_commits(symbol
, diff
):
190 """Find commits changing %symbol in the given range of %diff."""
191 commits
= execute("git log --pretty=oneline --abbrev-commit -G %s %s"
197 """Return true if the current working tree is dirty (i.e., if any file has
198 been added, deleted, modified, renamed or copied but not committed)."""
199 stdout
= execute("git status --porcelain")
201 if re
.findall(r
"[URMADC]{1}", line
[:2]):
207 """Return commit hash of current HEAD."""
208 stdout
= execute("git rev-parse HEAD")
209 return stdout
.strip('\n')
212 def check_symbols(ignore
):
213 """Find undefined Kconfig symbols and return a dict with the symbol as key
214 and a list of referencing files as value. Files matching %ignore are not
215 checked for undefined symbols."""
218 defined_features
= set()
219 referenced_features
= dict() # {feature: [files]}
221 # use 'git ls-files' to get the worklist
222 stdout
= execute("git ls-files")
223 if len(stdout
) > 0 and stdout
[-1] == "\n":
226 for gitfile
in stdout
.rsplit("\n"):
227 if ".git" in gitfile
or "ChangeLog" in gitfile
or \
228 ".log" in gitfile
or os
.path
.isdir(gitfile
) or \
229 gitfile
.startswith("tools/"):
231 if REGEX_FILE_KCONFIG
.match(gitfile
):
232 kconfig_files
.append(gitfile
)
234 # all non-Kconfig files are checked for consistency
235 source_files
.append(gitfile
)
237 for sfile
in source_files
:
238 if ignore
and re
.match(ignore
, sfile
):
239 # do not check files matching %ignore
241 parse_source_file(sfile
, referenced_features
)
243 for kfile
in kconfig_files
:
244 if ignore
and re
.match(ignore
, kfile
):
245 # do not collect references for files matching %ignore
246 parse_kconfig_file(kfile
, defined_features
, dict())
248 parse_kconfig_file(kfile
, defined_features
, referenced_features
)
250 undefined
= {} # {feature: [files]}
251 for feature
in sorted(referenced_features
):
252 # filter some false positives
253 if feature
== "FOO" or feature
== "BAR" or \
254 feature
== "FOO_BAR" or feature
== "XXX":
256 if feature
not in defined_features
:
257 if feature
.endswith("_MODULE"):
258 # avoid false positives for kernel modules
259 if feature
[:-len("_MODULE")] in defined_features
:
261 undefined
[feature
] = referenced_features
.get(feature
)
265 def parse_source_file(sfile
, referenced_features
):
266 """Parse @sfile for referenced Kconfig features."""
268 with
open(sfile
, "r") as stream
:
269 lines
= stream
.readlines()
272 if not "CONFIG_" in line
:
274 features
= REGEX_SOURCE_FEATURE
.findall(line
)
275 for feature
in features
:
276 if not REGEX_FILTER_FEATURES
.search(feature
):
278 sfiles
= referenced_features
.get(feature
, set())
280 referenced_features
[feature
] = sfiles
283 def get_features_in_line(line
):
284 """Return mentioned Kconfig features in @line."""
285 return REGEX_FEATURE
.findall(line
)
288 def parse_kconfig_file(kfile
, defined_features
, referenced_features
):
289 """Parse @kfile and update feature definitions and references."""
293 with
open(kfile
, "r") as stream
:
294 lines
= stream
.readlines()
296 for i
in range(len(lines
)):
298 line
= line
.strip('\n')
299 line
= line
.split("#")[0] # ignore comments
301 if REGEX_KCONFIG_DEF
.match(line
):
302 feature_def
= REGEX_KCONFIG_DEF
.findall(line
)
303 defined_features
.add(feature_def
[0])
305 elif REGEX_KCONFIG_HELP
.match(line
):
308 # ignore content of help messages
310 elif REGEX_KCONFIG_STMT
.match(line
):
311 features
= get_features_in_line(line
)
312 # multi-line statements
313 while line
.endswith("\\"):
316 line
= line
.strip('\n')
317 features
.extend(get_features_in_line(line
))
318 for feature
in set(features
):
319 if REGEX_NUMERIC
.match(feature
):
320 # ignore numeric values
322 paths
= referenced_features
.get(feature
, set())
324 referenced_features
[feature
] = paths
327 if __name__
== "__main__":