3 """Find Kconfig symbols that are referenced but not defined."""
5 # (c) 2014-2015 Valentin Rothberg <Valentin.Rothberg@lip6.fr>
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 STMT
= r
"^\s*(?:if|select|depends\s+on)\s+" + EXPR
24 SOURCE_FEATURE
= r
"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE
+ r
")"
27 REGEX_FILE_KCONFIG
= re
.compile(r
".*Kconfig[\.\w+\-]*$")
28 REGEX_FEATURE
= re
.compile(r
"(" + FEATURE
+ r
")")
29 REGEX_SOURCE_FEATURE
= re
.compile(SOURCE_FEATURE
)
30 REGEX_KCONFIG_DEF
= re
.compile(DEF
)
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]$")
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."
48 parser
= OptionParser(usage
=usage
)
50 parser
.add_option('-c', '--commit', dest
='commit', action
='store',
52 help="Check if the specified commit (hash) introduces "
53 "undefined Kconfig symbols.")
55 parser
.add_option('-d', '--diff', dest
='diff', action
='store',
57 help="Diff undefined symbols between two commits. The "
58 "input format bases on Git log's "
59 "\'commmit1..commit2\'.")
61 parser
.add_option('-i', '--ignore', dest
='ignore', action
='store',
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'.")
67 parser
.add_option('', '--force', dest
='force', action
='store_true',
69 help="Reset current Git tree even when it's dirty.")
71 (opts
, _
) = parser
.parse_args()
73 if opts
.commit
and opts
.diff
:
74 sys
.exit("Please specify only one option at once.")
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\'")
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 "
91 re
.match(opts
.ignore
, "this/is/just/a/test.c")
93 sys
.exit("Please specify a valid Python regex.")
99 """Main function of this module."""
100 opts
= parse_options()
102 if opts
.commit
or opts
.diff
:
109 commit_a
= opts
.commit
+ "~"
110 commit_b
= opts
.commit
112 split
= opts
.diff
.split("..")
118 # get undefined items before the commit
119 execute("git reset --hard %s" % commit_a
)
120 undefined_a
= check_symbols(opts
.ignore
)
122 # get undefined items for the commit
123 execute("git reset --hard %s" % commit_b
)
124 undefined_b
= check_symbols(opts
.ignore
)
126 # report cases that are present for the commit but not before
127 for feature
in sorted(undefined_b
):
128 # feature has not been undefined before
129 if not feature
in undefined_a
:
130 files
= sorted(undefined_b
.get(feature
))
131 print "%s\t%s" % (feature
, ", ".join(files
))
132 # check if there are new files that reference the undefined feature
134 files
= sorted(undefined_b
.get(feature
) -
135 undefined_a
.get(feature
))
137 print "%s\t%s" % (feature
, ", ".join(files
))
140 execute("git reset --hard %s" % head
)
142 # default to check the entire tree
144 undefined
= check_symbols(opts
.ignore
)
145 for feature
in sorted(undefined
):
146 files
= sorted(undefined
.get(feature
))
147 print "%s\t%s" % (feature
, ", ".join(files
))
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:
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")
164 if re
.findall(r
"[URMADC]{1}", line
[:2]):
170 """Return commit hash of current HEAD."""
171 stdout
= execute("git rev-parse HEAD")
172 return stdout
.strip('\n')
175 def check_symbols(ignore
):
176 """Find undefined Kconfig symbols and return a dict with the symbol as key
177 and a list of referencing files as value. Files matching %ignore are not
178 checked for undefined symbols."""
181 defined_features
= set()
182 referenced_features
= dict() # {feature: [files]}
184 # use 'git ls-files' to get the worklist
185 stdout
= execute("git ls-files")
186 if len(stdout
) > 0 and stdout
[-1] == "\n":
189 for gitfile
in stdout
.rsplit("\n"):
190 if ".git" in gitfile
or "ChangeLog" in gitfile
or \
191 ".log" in gitfile
or os
.path
.isdir(gitfile
) or \
192 gitfile
.startswith("tools/"):
194 if REGEX_FILE_KCONFIG
.match(gitfile
):
195 kconfig_files
.append(gitfile
)
197 # all non-Kconfig files are checked for consistency
198 source_files
.append(gitfile
)
200 for sfile
in source_files
:
201 if ignore
and re
.match(ignore
, sfile
):
202 # do not check files matching %ignore
204 parse_source_file(sfile
, referenced_features
)
206 for kfile
in kconfig_files
:
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())
211 parse_kconfig_file(kfile
, defined_features
, referenced_features
)
213 undefined
= {} # {feature: [files]}
214 for feature
in sorted(referenced_features
):
215 # filter some false positives
216 if feature
== "FOO" or feature
== "BAR" or \
217 feature
== "FOO_BAR" or feature
== "XXX":
219 if feature
not in defined_features
:
220 if feature
.endswith("_MODULE"):
221 # avoid false positives for kernel modules
222 if feature
[:-len("_MODULE")] in defined_features
:
224 undefined
[feature
] = referenced_features
.get(feature
)
228 def parse_source_file(sfile
, referenced_features
):
229 """Parse @sfile for referenced Kconfig features."""
231 with
open(sfile
, "r") as stream
:
232 lines
= stream
.readlines()
235 if not "CONFIG_" in line
:
237 features
= REGEX_SOURCE_FEATURE
.findall(line
)
238 for feature
in features
:
239 if not REGEX_FILTER_FEATURES
.search(feature
):
241 sfiles
= referenced_features
.get(feature
, set())
243 referenced_features
[feature
] = sfiles
246 def get_features_in_line(line
):
247 """Return mentioned Kconfig features in @line."""
248 return REGEX_FEATURE
.findall(line
)
251 def parse_kconfig_file(kfile
, defined_features
, referenced_features
):
252 """Parse @kfile and update feature definitions and references."""
256 with
open(kfile
, "r") as stream
:
257 lines
= stream
.readlines()
259 for i
in range(len(lines
)):
261 line
= line
.strip('\n')
262 line
= line
.split("#")[0] # ignore comments
264 if REGEX_KCONFIG_DEF
.match(line
):
265 feature_def
= REGEX_KCONFIG_DEF
.findall(line
)
266 defined_features
.add(feature_def
[0])
268 elif REGEX_KCONFIG_HELP
.match(line
):
271 # ignore content of help messages
273 elif REGEX_KCONFIG_STMT
.match(line
):
274 features
= get_features_in_line(line
)
275 # multi-line statements
276 while line
.endswith("\\"):
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())
284 referenced_features
[feature
] = paths
287 if __name__
== "__main__":