checkkconfigsymbols.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. #!/usr/bin/env python3
  2. """Find Kconfig symbols that are referenced but not defined."""
  3. # (c) 2014-2016 Valentin Rothberg <valentinrothberg@gmail.com>
  4. # (c) 2014 Stefan Hengelein <stefan.hengelein@fau.de>
  5. #
  6. # Licensed under the terms of the GNU GPL License version 2
  7. import argparse
  8. import difflib
  9. import os
  10. import re
  11. import signal
  12. import subprocess
  13. import sys
  14. from multiprocessing import Pool, cpu_count
  15. from subprocess import Popen, PIPE, STDOUT
  16. # regex expressions
  17. OPERATORS = r"&|\(|\)|\||\!"
  18. FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}"
  19. DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*"
  20. EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+"
  21. DEFAULT = r"default\s+.*?(?:if\s.+){,1}"
  22. STMT = r"^\s*(?:if|select|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR
  23. SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")"
  24. # regex objects
  25. REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$")
  26. REGEX_FEATURE = re.compile(r'(?!\B)' + FEATURE + r'(?!\B)')
  27. REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE)
  28. REGEX_KCONFIG_DEF = re.compile(DEF)
  29. REGEX_KCONFIG_EXPR = re.compile(EXPR)
  30. REGEX_KCONFIG_STMT = re.compile(STMT)
  31. REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$")
  32. REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$")
  33. REGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+")
  34. REGEX_QUOTES = re.compile("(\"(.*?)\")")
  35. def parse_options():
  36. """The user interface of this module."""
  37. usage = "Run this tool to detect Kconfig symbols that are referenced but " \
  38. "not defined in Kconfig. If no option is specified, " \
  39. "checkkconfigsymbols defaults to check your current tree. " \
  40. "Please note that specifying commits will 'git reset --hard\' " \
  41. "your current tree! You may save uncommitted changes to avoid " \
  42. "losing data."
  43. parser = argparse.ArgumentParser(description=usage)
  44. parser.add_argument('-c', '--commit', dest='commit', action='store',
  45. default="",
  46. help="check if the specified commit (hash) introduces "
  47. "undefined Kconfig symbols")
  48. parser.add_argument('-d', '--diff', dest='diff', action='store',
  49. default="",
  50. help="diff undefined symbols between two commits "
  51. "(e.g., -d commmit1..commit2)")
  52. parser.add_argument('-f', '--find', dest='find', action='store_true',
  53. default=False,
  54. help="find and show commits that may cause symbols to be "
  55. "missing (required to run with --diff)")
  56. parser.add_argument('-i', '--ignore', dest='ignore', action='store',
  57. default="",
  58. help="ignore files matching this Python regex "
  59. "(e.g., -i '.*defconfig')")
  60. parser.add_argument('-s', '--sim', dest='sim', action='store', default="",
  61. help="print a list of max. 10 string-similar symbols")
  62. parser.add_argument('--force', dest='force', action='store_true',
  63. default=False,
  64. help="reset current Git tree even when it's dirty")
  65. parser.add_argument('--no-color', dest='color', action='store_false',
  66. default=True,
  67. help="don't print colored output (default when not "
  68. "outputting to a terminal)")
  69. args = parser.parse_args()
  70. if args.commit and args.diff:
  71. sys.exit("Please specify only one option at once.")
  72. if args.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", args.diff):
  73. sys.exit("Please specify valid input in the following format: "
  74. "\'commit1..commit2\'")
  75. if args.commit or args.diff:
  76. if not args.force and tree_is_dirty():
  77. sys.exit("The current Git tree is dirty (see 'git status'). "
  78. "Running this script may\ndelete important data since it "
  79. "calls 'git reset --hard' for some performance\nreasons. "
  80. " Please run this script in a clean Git tree or pass "
  81. "'--force' if you\nwant to ignore this warning and "
  82. "continue.")
  83. if args.commit:
  84. args.find = False
  85. if args.ignore:
  86. try:
  87. re.match(args.ignore, "this/is/just/a/test.c")
  88. except:
  89. sys.exit("Please specify a valid Python regex.")
  90. return args
  91. def main():
  92. """Main function of this module."""
  93. args = parse_options()
  94. global color
  95. color = args.color and sys.stdout.isatty()
  96. if args.sim and not args.commit and not args.diff:
  97. sims = find_sims(args.sim, args.ignore)
  98. if sims:
  99. print("%s: %s" % (yel("Similar symbols"), ', '.join(sims)))
  100. else:
  101. print("%s: no similar symbols found" % yel("Similar symbols"))
  102. sys.exit(0)
  103. # dictionary of (un)defined symbols
  104. defined = {}
  105. undefined = {}
  106. if args.commit or args.diff:
  107. head = get_head()
  108. # get commit range
  109. commit_a = None
  110. commit_b = None
  111. if args.commit:
  112. commit_a = args.commit + "~"
  113. commit_b = args.commit
  114. elif args.diff:
  115. split = args.diff.split("..")
  116. commit_a = split[0]
  117. commit_b = split[1]
  118. undefined_a = {}
  119. undefined_b = {}
  120. # get undefined items before the commit
  121. execute("git reset --hard %s" % commit_a)
  122. undefined_a, _ = check_symbols(args.ignore)
  123. # get undefined items for the commit
  124. execute("git reset --hard %s" % commit_b)
  125. undefined_b, defined = check_symbols(args.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. undefined[feature] = files
  132. # check if there are new files that reference the undefined feature
  133. else:
  134. files = sorted(undefined_b.get(feature) -
  135. undefined_a.get(feature))
  136. if files:
  137. undefined[feature] = files
  138. # reset to head
  139. execute("git reset --hard %s" % head)
  140. # default to check the entire tree
  141. else:
  142. undefined, defined = check_symbols(args.ignore)
  143. # now print the output
  144. for feature in sorted(undefined):
  145. print(red(feature))
  146. files = sorted(undefined.get(feature))
  147. print("%s: %s" % (yel("Referencing files"), ", ".join(files)))
  148. sims = find_sims(feature, args.ignore, defined)
  149. sims_out = yel("Similar symbols")
  150. if sims:
  151. print("%s: %s" % (sims_out, ', '.join(sims)))
  152. else:
  153. print("%s: %s" % (sims_out, "no similar symbols found"))
  154. if args.find:
  155. print("%s:" % yel("Commits changing symbol"))
  156. commits = find_commits(feature, args.diff)
  157. if commits:
  158. for commit in commits:
  159. commit = commit.split(" ", 1)
  160. print("\t- %s (\"%s\")" % (yel(commit[0]), commit[1]))
  161. else:
  162. print("\t- no commit found")
  163. print() # new line
  164. def yel(string):
  165. """
  166. Color %string yellow.
  167. """
  168. return "\033[33m%s\033[0m" % string if color else string
  169. def red(string):
  170. """
  171. Color %string red.
  172. """
  173. return "\033[31m%s\033[0m" % string if color else string
  174. def execute(cmd):
  175. """Execute %cmd and return stdout. Exit in case of error."""
  176. try:
  177. cmdlist = cmd.split(" ")
  178. stdout = subprocess.check_output(cmdlist, stderr=subprocess.STDOUT, shell=False)
  179. stdout = stdout.decode(errors='replace')
  180. except subprocess.CalledProcessError as fail:
  181. exit("Failed to execute %s\n%s" % (cmd, fail))
  182. return stdout
  183. def find_commits(symbol, diff):
  184. """Find commits changing %symbol in the given range of %diff."""
  185. commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s"
  186. % (symbol, diff))
  187. return [x for x in commits.split("\n") if x]
  188. def tree_is_dirty():
  189. """Return true if the current working tree is dirty (i.e., if any file has
  190. been added, deleted, modified, renamed or copied but not committed)."""
  191. stdout = execute("git status --porcelain")
  192. for line in stdout:
  193. if re.findall(r"[URMADC]{1}", line[:2]):
  194. return True
  195. return False
  196. def get_head():
  197. """Return commit hash of current HEAD."""
  198. stdout = execute("git rev-parse HEAD")
  199. return stdout.strip('\n')
  200. def partition(lst, size):
  201. """Partition list @lst into eveni-sized lists of size @size."""
  202. return [lst[i::size] for i in range(size)]
  203. def init_worker():
  204. """Set signal handler to ignore SIGINT."""
  205. signal.signal(signal.SIGINT, signal.SIG_IGN)
  206. def find_sims(symbol, ignore, defined = []):
  207. """Return a list of max. ten Kconfig symbols that are string-similar to
  208. @symbol."""
  209. if defined:
  210. return sorted(difflib.get_close_matches(symbol, set(defined), 10))
  211. pool = Pool(cpu_count(), init_worker)
  212. kfiles = []
  213. for gitfile in get_files():
  214. if REGEX_FILE_KCONFIG.match(gitfile):
  215. kfiles.append(gitfile)
  216. arglist = []
  217. for part in partition(kfiles, cpu_count()):
  218. arglist.append((part, ignore))
  219. for res in pool.map(parse_kconfig_files, arglist):
  220. defined.extend(res[0])
  221. return sorted(difflib.get_close_matches(symbol, set(defined), 10))
  222. def get_files():
  223. """Return a list of all files in the current git directory."""
  224. # use 'git ls-files' to get the worklist
  225. stdout = execute("git ls-files")
  226. if len(stdout) > 0 and stdout[-1] == "\n":
  227. stdout = stdout[:-1]
  228. files = []
  229. for gitfile in stdout.rsplit("\n"):
  230. if ".git" in gitfile or "ChangeLog" in gitfile or \
  231. ".log" in gitfile or os.path.isdir(gitfile) or \
  232. gitfile.startswith("tools/"):
  233. continue
  234. files.append(gitfile)
  235. return files
  236. def check_symbols(ignore):
  237. """Find undefined Kconfig symbols and return a dict with the symbol as key
  238. and a list of referencing files as value. Files matching %ignore are not
  239. checked for undefined symbols."""
  240. pool = Pool(cpu_count(), init_worker)
  241. try:
  242. return check_symbols_helper(pool, ignore)
  243. except KeyboardInterrupt:
  244. pool.terminate()
  245. pool.join()
  246. sys.exit(1)
  247. def check_symbols_helper(pool, ignore):
  248. """Helper method for check_symbols(). Used to catch keyboard interrupts in
  249. check_symbols() in order to properly terminate running worker processes."""
  250. source_files = []
  251. kconfig_files = []
  252. defined_features = []
  253. referenced_features = dict() # {file: [features]}
  254. for gitfile in get_files():
  255. if REGEX_FILE_KCONFIG.match(gitfile):
  256. kconfig_files.append(gitfile)
  257. else:
  258. if ignore and not re.match(ignore, gitfile):
  259. continue
  260. # add source files that do not match the ignore pattern
  261. source_files.append(gitfile)
  262. # parse source files
  263. arglist = partition(source_files, cpu_count())
  264. for res in pool.map(parse_source_files, arglist):
  265. referenced_features.update(res)
  266. # parse kconfig files
  267. arglist = []
  268. for part in partition(kconfig_files, cpu_count()):
  269. arglist.append((part, ignore))
  270. for res in pool.map(parse_kconfig_files, arglist):
  271. defined_features.extend(res[0])
  272. referenced_features.update(res[1])
  273. defined_features = set(defined_features)
  274. # inverse mapping of referenced_features to dict(feature: [files])
  275. inv_map = dict()
  276. for _file, features in referenced_features.items():
  277. for feature in features:
  278. inv_map[feature] = inv_map.get(feature, set())
  279. inv_map[feature].add(_file)
  280. referenced_features = inv_map
  281. undefined = {} # {feature: [files]}
  282. for feature in sorted(referenced_features):
  283. # filter some false positives
  284. if feature == "FOO" or feature == "BAR" or \
  285. feature == "FOO_BAR" or feature == "XXX":
  286. continue
  287. if feature not in defined_features:
  288. if feature.endswith("_MODULE"):
  289. # avoid false positives for kernel modules
  290. if feature[:-len("_MODULE")] in defined_features:
  291. continue
  292. undefined[feature] = referenced_features.get(feature)
  293. return undefined, defined_features
  294. def parse_source_files(source_files):
  295. """Parse each source file in @source_files and return dictionary with source
  296. files as keys and lists of references Kconfig symbols as values."""
  297. referenced_features = dict()
  298. for sfile in source_files:
  299. referenced_features[sfile] = parse_source_file(sfile)
  300. return referenced_features
  301. def parse_source_file(sfile):
  302. """Parse @sfile and return a list of referenced Kconfig features."""
  303. lines = []
  304. references = []
  305. if not os.path.exists(sfile):
  306. return references
  307. with open(sfile, "r", encoding='utf-8', errors='replace') as stream:
  308. lines = stream.readlines()
  309. for line in lines:
  310. if not "CONFIG_" in line:
  311. continue
  312. features = REGEX_SOURCE_FEATURE.findall(line)
  313. for feature in features:
  314. if not REGEX_FILTER_FEATURES.search(feature):
  315. continue
  316. references.append(feature)
  317. return references
  318. def get_features_in_line(line):
  319. """Return mentioned Kconfig features in @line."""
  320. return REGEX_FEATURE.findall(line)
  321. def parse_kconfig_files(args):
  322. """Parse kconfig files and return tuple of defined and references Kconfig
  323. symbols. Note, @args is a tuple of a list of files and the @ignore
  324. pattern."""
  325. kconfig_files = args[0]
  326. ignore = args[1]
  327. defined_features = []
  328. referenced_features = dict()
  329. for kfile in kconfig_files:
  330. defined, references = parse_kconfig_file(kfile)
  331. defined_features.extend(defined)
  332. if ignore and re.match(ignore, kfile):
  333. # do not collect references for files that match the ignore pattern
  334. continue
  335. referenced_features[kfile] = references
  336. return (defined_features, referenced_features)
  337. def parse_kconfig_file(kfile):
  338. """Parse @kfile and update feature definitions and references."""
  339. lines = []
  340. defined = []
  341. references = []
  342. skip = False
  343. if not os.path.exists(kfile):
  344. return defined, references
  345. with open(kfile, "r", encoding='utf-8', errors='replace') as stream:
  346. lines = stream.readlines()
  347. for i in range(len(lines)):
  348. line = lines[i]
  349. line = line.strip('\n')
  350. line = line.split("#")[0] # ignore comments
  351. if REGEX_KCONFIG_DEF.match(line):
  352. feature_def = REGEX_KCONFIG_DEF.findall(line)
  353. defined.append(feature_def[0])
  354. skip = False
  355. elif REGEX_KCONFIG_HELP.match(line):
  356. skip = True
  357. elif skip:
  358. # ignore content of help messages
  359. pass
  360. elif REGEX_KCONFIG_STMT.match(line):
  361. line = REGEX_QUOTES.sub("", line)
  362. features = get_features_in_line(line)
  363. # multi-line statements
  364. while line.endswith("\\"):
  365. i += 1
  366. line = lines[i]
  367. line = line.strip('\n')
  368. features.extend(get_features_in_line(line))
  369. for feature in set(features):
  370. if REGEX_NUMERIC.match(feature):
  371. # ignore numeric values
  372. continue
  373. references.append(feature)
  374. return defined, references
  375. if __name__ == "__main__":
  376. main()