2 # SPDX-License-Identifier: GPL-2.0-only
4 """Find Kconfig symbols that are referenced but not defined."""
6 # (c) 2014-2017 Valentin Rothberg <valentinrothberg@gmail.com>
7 # (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
18 from multiprocessing
import Pool
, cpu_count
22 OPERATORS
= r
"&|\(|\)|\||\!"
23 SYMBOL
= r
"(?:\w*[A-Z0-9]\w*){2,}"
24 DEF
= r
"^\s*(?:menu){,1}config\s+(" + SYMBOL
+ r
")\s*"
25 EXPR
= r
"(?:" + OPERATORS
+ r
"|\s|" + SYMBOL
+ r
")+"
26 DEFAULT
= r
"default\s+.*?(?:if\s.+){,1}"
27 STMT
= r
"^\s*(?:if|select|imply|depends\s+on|(?:" + DEFAULT
+ r
"))\s+" + EXPR
28 SOURCE_SYMBOL
= r
"(?:\W|\b)+[D]{,1}CONFIG_(" + SYMBOL
+ r
")"
31 REGEX_FILE_KCONFIG
= re
.compile(r
".*Kconfig[\.\w+\-]*$")
32 REGEX_SYMBOL
= re
.compile(r
'(?!\B)' + SYMBOL
+ r
'(?!\B)')
33 REGEX_SOURCE_SYMBOL
= re
.compile(SOURCE_SYMBOL
)
34 REGEX_KCONFIG_DEF
= re
.compile(DEF
)
35 REGEX_KCONFIG_EXPR
= re
.compile(EXPR
)
36 REGEX_KCONFIG_STMT
= re
.compile(STMT
)
37 REGEX_KCONFIG_HELP
= re
.compile(r
"^\s+help\s*$")
38 REGEX_FILTER_SYMBOLS
= re
.compile(r
"[A-Za-z0-9]$")
39 REGEX_NUMERIC
= re
.compile(r
"0[xX][0-9a-fA-F]+|[0-9]+")
40 REGEX_QUOTES
= re
.compile("(\"(.*?)\")")
44 """The user interface of this module."""
45 usage
= "Run this tool to detect Kconfig symbols that are referenced but " \
46 "not defined in Kconfig. If no option is specified, " \
47 "checkkconfigsymbols defaults to check your current tree. " \
48 "Please note that specifying commits will 'git reset --hard\' " \
49 "your current tree! You may save uncommitted changes to avoid " \
52 parser
= argparse
.ArgumentParser(description
=usage
)
54 parser
.add_argument('-c', '--commit', dest
='commit', action
='store',
56 help="check if the specified commit (hash) introduces "
57 "undefined Kconfig symbols")
59 parser
.add_argument('-d', '--diff', dest
='diff', action
='store',
61 help="diff undefined symbols between two commits "
62 "(e.g., -d commmit1..commit2)")
64 parser
.add_argument('-f', '--find', dest
='find', action
='store_true',
66 help="find and show commits that may cause symbols to be "
67 "missing (required to run with --diff)")
69 parser
.add_argument('-i', '--ignore', dest
='ignore', action
='store',
71 help="ignore files matching this Python regex "
72 "(e.g., -i '.*defconfig')")
74 parser
.add_argument('-s', '--sim', dest
='sim', action
='store', default
="",
75 help="print a list of max. 10 string-similar symbols")
77 parser
.add_argument('--force', dest
='force', action
='store_true',
79 help="reset current Git tree even when it's dirty")
81 parser
.add_argument('--no-color', dest
='color', action
='store_false',
83 help="don't print colored output (default when not "
84 "outputting to a terminal)")
86 args
= parser
.parse_args()
88 if args
.commit
and args
.diff
:
89 sys
.exit("Please specify only one option at once.")
91 if args
.diff
and not re
.match(r
"^[\w\-\.\^]+\.\.[\w\-\.\^]+$", args
.diff
):
92 sys
.exit("Please specify valid input in the following format: "
93 "\'commit1..commit2\'")
95 if args
.commit
or args
.diff
:
96 if not args
.force
and tree_is_dirty():
97 sys
.exit("The current Git tree is dirty (see 'git status'). "
98 "Running this script may\ndelete important data since it "
99 "calls 'git reset --hard' for some performance\nreasons. "
100 " Please run this script in a clean Git tree or pass "
101 "'--force' if you\nwant to ignore this warning and "
109 re
.match(args
.ignore
, "this/is/just/a/test.c")
111 sys
.exit("Please specify a valid Python regex.")
117 """Main function of this module."""
118 args
= parse_options()
121 COLOR
= args
.color
and sys
.stdout
.isatty()
123 if args
.sim
and not args
.commit
and not args
.diff
:
124 sims
= find_sims(args
.sim
, args
.ignore
)
126 print("%s: %s" % (yel("Similar symbols"), ', '.join(sims
)))
128 print("%s: no similar symbols found" % yel("Similar symbols"))
131 # dictionary of (un)defined symbols
135 if args
.commit
or args
.diff
:
142 commit_a
= args
.commit
+ "~"
143 commit_b
= args
.commit
145 split
= args
.diff
.split("..")
151 # get undefined items before the commit
153 undefined_a
, _
= check_symbols(args
.ignore
)
155 # get undefined items for the commit
157 undefined_b
, defined
= check_symbols(args
.ignore
)
159 # report cases that are present for the commit but not before
160 for symbol
in sorted(undefined_b
):
161 # symbol has not been undefined before
162 if symbol
not in undefined_a
:
163 files
= sorted(undefined_b
.get(symbol
))
164 undefined
[symbol
] = files
165 # check if there are new files that reference the undefined symbol
167 files
= sorted(undefined_b
.get(symbol
) -
168 undefined_a
.get(symbol
))
170 undefined
[symbol
] = files
175 # default to check the entire tree
177 undefined
, defined
= check_symbols(args
.ignore
)
179 # now print the output
180 for symbol
in sorted(undefined
):
183 files
= sorted(undefined
.get(symbol
))
184 print("%s: %s" % (yel("Referencing files"), ", ".join(files
)))
186 sims
= find_sims(symbol
, args
.ignore
, defined
)
187 sims_out
= yel("Similar symbols")
189 print("%s: %s" % (sims_out
, ', '.join(sims
)))
191 print("%s: %s" % (sims_out
, "no similar symbols found"))
194 print("%s:" % yel("Commits changing symbol"))
195 commits
= find_commits(symbol
, args
.diff
)
197 for commit
in commits
:
198 commit
= commit
.split(" ", 1)
199 print("\t- %s (\"%s\")" % (yel(commit
[0]), commit
[1]))
201 print("\t- no commit found")
206 """Reset current git tree to %commit."""
207 execute(["git", "reset", "--hard", commit
])
212 Color %string yellow.
214 return "\033[33m%s\033[0m" % string
if COLOR
else string
221 return "\033[31m%s\033[0m" % string
if COLOR
else string
225 """Execute %cmd and return stdout. Exit in case of error."""
227 stdout
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
, shell
=False)
228 stdout
= stdout
.decode(errors
='replace')
229 except subprocess
.CalledProcessError
as fail
:
234 def find_commits(symbol
, diff
):
235 """Find commits changing %symbol in the given range of %diff."""
236 commits
= execute(["git", "log", "--pretty=oneline",
237 "--abbrev-commit", "-G",
239 return [x
for x
in commits
.split("\n") if x
]
243 """Return true if the current working tree is dirty (i.e., if any file has
244 been added, deleted, modified, renamed or copied but not committed)."""
245 stdout
= execute(["git", "status", "--porcelain"])
247 if re
.findall(r
"[URMADC]{1}", line
[:2]):
253 """Return commit hash of current HEAD."""
254 stdout
= execute(["git", "rev-parse", "HEAD"])
255 return stdout
.strip('\n')
258 def partition(lst
, size
):
259 """Partition list @lst into eveni-sized lists of size @size."""
260 return [lst
[i
::size
] for i
in range(size
)]
264 """Set signal handler to ignore SIGINT."""
265 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
268 def find_sims(symbol
, ignore
, defined
=[]):
269 """Return a list of max. ten Kconfig symbols that are string-similar to
272 return difflib
.get_close_matches(symbol
, set(defined
), 10)
274 pool
= Pool(cpu_count(), init_worker
)
276 for gitfile
in get_files():
277 if REGEX_FILE_KCONFIG
.match(gitfile
):
278 kfiles
.append(gitfile
)
281 for part
in partition(kfiles
, cpu_count()):
282 arglist
.append((part
, ignore
))
284 for res
in pool
.map(parse_kconfig_files
, arglist
):
285 defined
.extend(res
[0])
287 return difflib
.get_close_matches(symbol
, set(defined
), 10)
291 """Return a list of all files in the current git directory."""
292 # use 'git ls-files' to get the worklist
293 stdout
= execute(["git", "ls-files"])
294 if len(stdout
) > 0 and stdout
[-1] == "\n":
298 for gitfile
in stdout
.rsplit("\n"):
299 if ".git" in gitfile
or "ChangeLog" in gitfile
or \
300 ".log" in gitfile
or os
.path
.isdir(gitfile
) or \
301 gitfile
.startswith("tools/"):
303 files
.append(gitfile
)
307 def check_symbols(ignore
):
308 """Find undefined Kconfig symbols and return a dict with the symbol as key
309 and a list of referencing files as value. Files matching %ignore are not
310 checked for undefined symbols."""
311 pool
= Pool(cpu_count(), init_worker
)
313 return check_symbols_helper(pool
, ignore
)
314 except KeyboardInterrupt:
320 def check_symbols_helper(pool
, ignore
):
321 """Helper method for check_symbols(). Used to catch keyboard interrupts in
322 check_symbols() in order to properly terminate running worker processes."""
326 referenced_symbols
= dict() # {file: [symbols]}
328 for gitfile
in get_files():
329 if REGEX_FILE_KCONFIG
.match(gitfile
):
330 kconfig_files
.append(gitfile
)
332 if ignore
and not re
.match(ignore
, gitfile
):
334 # add source files that do not match the ignore pattern
335 source_files
.append(gitfile
)
338 arglist
= partition(source_files
, cpu_count())
339 for res
in pool
.map(parse_source_files
, arglist
):
340 referenced_symbols
.update(res
)
342 # parse kconfig files
344 for part
in partition(kconfig_files
, cpu_count()):
345 arglist
.append((part
, ignore
))
346 for res
in pool
.map(parse_kconfig_files
, arglist
):
347 defined_symbols
.extend(res
[0])
348 referenced_symbols
.update(res
[1])
349 defined_symbols
= set(defined_symbols
)
351 # inverse mapping of referenced_symbols to dict(symbol: [files])
353 for _file
, symbols
in referenced_symbols
.items():
354 for symbol
in symbols
:
355 inv_map
[symbol
] = inv_map
.get(symbol
, set())
356 inv_map
[symbol
].add(_file
)
357 referenced_symbols
= inv_map
359 undefined
= {} # {symbol: [files]}
360 for symbol
in sorted(referenced_symbols
):
361 # filter some false positives
362 if symbol
== "FOO" or symbol
== "BAR" or \
363 symbol
== "FOO_BAR" or symbol
== "XXX":
365 if symbol
not in defined_symbols
:
366 if symbol
.endswith("_MODULE"):
367 # avoid false positives for kernel modules
368 if symbol
[:-len("_MODULE")] in defined_symbols
:
370 undefined
[symbol
] = referenced_symbols
.get(symbol
)
371 return undefined
, defined_symbols
374 def parse_source_files(source_files
):
375 """Parse each source file in @source_files and return dictionary with source
376 files as keys and lists of references Kconfig symbols as values."""
377 referenced_symbols
= dict()
378 for sfile
in source_files
:
379 referenced_symbols
[sfile
] = parse_source_file(sfile
)
380 return referenced_symbols
383 def parse_source_file(sfile
):
384 """Parse @sfile and return a list of referenced Kconfig symbols."""
388 if not os
.path
.exists(sfile
):
391 with
open(sfile
, "r", encoding
='utf-8', errors
='replace') as stream
:
392 lines
= stream
.readlines()
395 if "CONFIG_" not in line
:
397 symbols
= REGEX_SOURCE_SYMBOL
.findall(line
)
398 for symbol
in symbols
:
399 if not REGEX_FILTER_SYMBOLS
.search(symbol
):
401 references
.append(symbol
)
406 def get_symbols_in_line(line
):
407 """Return mentioned Kconfig symbols in @line."""
408 return REGEX_SYMBOL
.findall(line
)
411 def parse_kconfig_files(args
):
412 """Parse kconfig files and return tuple of defined and references Kconfig
413 symbols. Note, @args is a tuple of a list of files and the @ignore
415 kconfig_files
= args
[0]
418 referenced_symbols
= dict()
420 for kfile
in kconfig_files
:
421 defined
, references
= parse_kconfig_file(kfile
)
422 defined_symbols
.extend(defined
)
423 if ignore
and re
.match(ignore
, kfile
):
424 # do not collect references for files that match the ignore pattern
426 referenced_symbols
[kfile
] = references
427 return (defined_symbols
, referenced_symbols
)
430 def parse_kconfig_file(kfile
):
431 """Parse @kfile and update symbol definitions and references."""
437 if not os
.path
.exists(kfile
):
438 return defined
, references
440 with
open(kfile
, "r", encoding
='utf-8', errors
='replace') as stream
:
441 lines
= stream
.readlines()
443 for i
in range(len(lines
)):
445 line
= line
.strip('\n')
446 line
= line
.split("#")[0] # ignore comments
448 if REGEX_KCONFIG_DEF
.match(line
):
449 symbol_def
= REGEX_KCONFIG_DEF
.findall(line
)
450 defined
.append(symbol_def
[0])
452 elif REGEX_KCONFIG_HELP
.match(line
):
455 # ignore content of help messages
457 elif REGEX_KCONFIG_STMT
.match(line
):
458 line
= REGEX_QUOTES
.sub("", line
)
459 symbols
= get_symbols_in_line(line
)
460 # multi-line statements
461 while line
.endswith("\\"):
464 line
= line
.strip('\n')
465 symbols
.extend(get_symbols_in_line(line
))
466 for symbol
in set(symbols
):
467 if REGEX_NUMERIC
.match(symbol
):
468 # ignore numeric values
470 references
.append(symbol
)
472 return defined
, references
475 if __name__
== "__main__":