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('', '--force', dest
='force', action
='store_true',
63 help="Reset current Git tree even when it's dirty.")
65 (opts
, _
) = parser
.parse_args()
67 if opts
.commit
and opts
.diff
:
68 sys
.exit("Please specify only one option at once.")
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\'")
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 "
87 """Main function of this module."""
88 opts
= parse_options()
90 if opts
.commit
or opts
.diff
:
97 commit_a
= opts
.commit
+ "~"
98 commit_b
= opts
.commit
100 split
= opts
.diff
.split("..")
106 # get undefined items before the commit
107 execute("git reset --hard %s" % commit_a
)
108 undefined_a
= check_symbols()
110 # get undefined items for the commit
111 execute("git reset --hard %s" % commit_b
)
112 undefined_b
= check_symbols()
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
122 files
= sorted(undefined_b
.get(feature
) -
123 undefined_a
.get(feature
))
125 print "%s\t%s" % (feature
, ", ".join(files
))
128 execute("git reset --hard %s" % head
)
130 # default to check the entire tree
132 undefined
= check_symbols()
133 for feature
in sorted(undefined
):
134 files
= sorted(undefined
.get(feature
))
135 print "%s\t%s" % (feature
, ", ".join(files
))
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:
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")
152 if re
.findall(r
"[URMADC]{1}", line
[:2]):
158 """Return commit hash of current HEAD."""
159 stdout
= execute("git rev-parse HEAD")
160 return stdout
.strip('\n')
164 """Find undefined Kconfig symbols and return a dict with the symbol as key
165 and a list of referencing files as value."""
168 defined_features
= set()
169 referenced_features
= dict() # {feature: [files]}
171 # use 'git ls-files' to get the worklist
172 stdout
= execute("git ls-files")
173 if len(stdout
) > 0 and stdout
[-1] == "\n":
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/"):
181 if REGEX_FILE_KCONFIG
.match(gitfile
):
182 kconfig_files
.append(gitfile
)
184 # all non-Kconfig files are checked for consistency
185 source_files
.append(gitfile
)
187 for sfile
in source_files
:
188 parse_source_file(sfile
, referenced_features
)
190 for kfile
in kconfig_files
:
191 parse_kconfig_file(kfile
, defined_features
, referenced_features
)
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":
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
:
204 undefined
[feature
] = referenced_features
.get(feature
)
208 def parse_source_file(sfile
, referenced_features
):
209 """Parse @sfile for referenced Kconfig features."""
211 with
open(sfile
, "r") as stream
:
212 lines
= stream
.readlines()
215 if not "CONFIG_" in line
:
217 features
= REGEX_SOURCE_FEATURE
.findall(line
)
218 for feature
in features
:
219 if not REGEX_FILTER_FEATURES
.search(feature
):
221 sfiles
= referenced_features
.get(feature
, set())
223 referenced_features
[feature
] = sfiles
226 def get_features_in_line(line
):
227 """Return mentioned Kconfig features in @line."""
228 return REGEX_FEATURE
.findall(line
)
231 def parse_kconfig_file(kfile
, defined_features
, referenced_features
):
232 """Parse @kfile and update feature definitions and references."""
236 with
open(kfile
, "r") as stream
:
237 lines
= stream
.readlines()
239 for i
in range(len(lines
)):
241 line
= line
.strip('\n')
242 line
= line
.split("#")[0] # ignore comments
244 if REGEX_KCONFIG_DEF
.match(line
):
245 feature_def
= REGEX_KCONFIG_DEF
.findall(line
)
246 defined_features
.add(feature_def
[0])
248 elif REGEX_KCONFIG_HELP
.match(line
):
251 # ignore content of help messages
253 elif REGEX_KCONFIG_STMT
.match(line
):
254 features
= get_features_in_line(line
)
255 # multi-line statements
256 while line
.endswith("\\"):
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())
264 referenced_features
[feature
] = paths
267 if __name__
== "__main__":