checkpatch.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # SPDX-License-Identifier: GPL-2.0+
  2. # Copyright (c) 2011 The Chromium OS Authors.
  3. #
  4. import collections
  5. import concurrent.futures
  6. import os
  7. import re
  8. import sys
  9. from patman import gitutil
  10. from u_boot_pylib import command
  11. from u_boot_pylib import terminal
  12. EMACS_PREFIX = r'(?:[0-9]{4}.*\.patch:[0-9]+: )?'
  13. TYPE_NAME = r'([A-Z_]+:)?'
  14. RE_ERROR = re.compile(r'ERROR:%s (.*)' % TYPE_NAME)
  15. RE_WARNING = re.compile(EMACS_PREFIX + r'WARNING:%s (.*)' % TYPE_NAME)
  16. RE_CHECK = re.compile(r'CHECK:%s (.*)' % TYPE_NAME)
  17. RE_FILE = re.compile(r'#(\d+): (FILE: ([^:]*):(\d+):)?')
  18. RE_NOTE = re.compile(r'NOTE: (.*)')
  19. def find_check_patch():
  20. top_level = gitutil.get_top_level()
  21. try_list = [
  22. os.getcwd(),
  23. os.path.join(os.getcwd(), '..', '..'),
  24. os.path.join(top_level, 'tools'),
  25. os.path.join(top_level, 'scripts'),
  26. '%s/bin' % os.getenv('HOME'),
  27. ]
  28. # Look in current dir
  29. for path in try_list:
  30. fname = os.path.join(path, 'checkpatch.pl')
  31. if os.path.isfile(fname):
  32. return fname
  33. # Look upwwards for a Chrome OS tree
  34. while not os.path.ismount(path):
  35. fname = os.path.join(path, 'src', 'third_party', 'kernel', 'files',
  36. 'scripts', 'checkpatch.pl')
  37. if os.path.isfile(fname):
  38. return fname
  39. path = os.path.dirname(path)
  40. sys.exit('Cannot find checkpatch.pl - please put it in your ' +
  41. '~/bin directory or use --no-check')
  42. def check_patch_parse_one_message(message):
  43. """Parse one checkpatch message
  44. Args:
  45. message: string to parse
  46. Returns:
  47. dict:
  48. 'type'; error or warning
  49. 'msg': text message
  50. 'file' : filename
  51. 'line': line number
  52. """
  53. if RE_NOTE.match(message):
  54. return {}
  55. item = {}
  56. err_match = RE_ERROR.match(message)
  57. warn_match = RE_WARNING.match(message)
  58. check_match = RE_CHECK.match(message)
  59. if err_match:
  60. item['cptype'] = err_match.group(1)
  61. item['msg'] = err_match.group(2)
  62. item['type'] = 'error'
  63. elif warn_match:
  64. item['cptype'] = warn_match.group(1)
  65. item['msg'] = warn_match.group(2)
  66. item['type'] = 'warning'
  67. elif check_match:
  68. item['cptype'] = check_match.group(1)
  69. item['msg'] = check_match.group(2)
  70. item['type'] = 'check'
  71. else:
  72. message_indent = ' '
  73. print('patman: failed to parse checkpatch message:\n%s' %
  74. (message_indent + message.replace('\n', '\n' + message_indent)),
  75. file=sys.stderr)
  76. return {}
  77. file_match = RE_FILE.search(message)
  78. # some messages have no file, catch those here
  79. no_file_match = any(s in message for s in [
  80. '\nSubject:', 'Missing Signed-off-by: line(s)',
  81. 'does MAINTAINERS need updating'
  82. ])
  83. if file_match:
  84. err_fname = file_match.group(3)
  85. if err_fname:
  86. item['file'] = err_fname
  87. item['line'] = int(file_match.group(4))
  88. else:
  89. item['file'] = '<patch>'
  90. item['line'] = int(file_match.group(1))
  91. elif no_file_match:
  92. item['file'] = '<patch>'
  93. else:
  94. message_indent = ' '
  95. print('patman: failed to find file / line information:\n%s' %
  96. (message_indent + message.replace('\n', '\n' + message_indent)),
  97. file=sys.stderr)
  98. return item
  99. def check_patch_parse(checkpatch_output, verbose=False):
  100. """Parse checkpatch.pl output
  101. Args:
  102. checkpatch_output: string to parse
  103. verbose: True to print out every line of the checkpatch output as it is
  104. parsed
  105. Returns:
  106. namedtuple containing:
  107. ok: False=failure, True=ok
  108. problems (list of problems): each a dict:
  109. 'type'; error or warning
  110. 'msg': text message
  111. 'file' : filename
  112. 'line': line number
  113. errors: Number of errors
  114. warnings: Number of warnings
  115. checks: Number of checks
  116. lines: Number of lines
  117. stdout: checkpatch_output
  118. """
  119. fields = ['ok', 'problems', 'errors', 'warnings', 'checks', 'lines',
  120. 'stdout']
  121. result = collections.namedtuple('CheckPatchResult', fields)
  122. result.stdout = checkpatch_output
  123. result.ok = False
  124. result.errors, result.warnings, result.checks = 0, 0, 0
  125. result.lines = 0
  126. result.problems = []
  127. # total: 0 errors, 0 warnings, 159 lines checked
  128. # or:
  129. # total: 0 errors, 2 warnings, 7 checks, 473 lines checked
  130. emacs_stats = r'(?:[0-9]{4}.*\.patch )?'
  131. re_stats = re.compile(emacs_stats +
  132. r'total: (\d+) errors, (\d+) warnings, (\d+)')
  133. re_stats_full = re.compile(emacs_stats +
  134. r'total: (\d+) errors, (\d+) warnings, (\d+)'
  135. r' checks, (\d+)')
  136. re_ok = re.compile(r'.*has no obvious style problems')
  137. re_bad = re.compile(r'.*has style problems, please review')
  138. # A blank line indicates the end of a message
  139. for message in result.stdout.split('\n\n'):
  140. if verbose:
  141. print(message)
  142. # either find stats, the verdict, or delegate
  143. match = re_stats_full.match(message)
  144. if not match:
  145. match = re_stats.match(message)
  146. if match:
  147. result.errors = int(match.group(1))
  148. result.warnings = int(match.group(2))
  149. if len(match.groups()) == 4:
  150. result.checks = int(match.group(3))
  151. result.lines = int(match.group(4))
  152. else:
  153. result.lines = int(match.group(3))
  154. elif re_ok.match(message):
  155. result.ok = True
  156. elif re_bad.match(message):
  157. result.ok = False
  158. else:
  159. problem = check_patch_parse_one_message(message)
  160. if problem:
  161. result.problems.append(problem)
  162. return result
  163. def check_patch(fname, verbose=False, show_types=False, use_tree=False):
  164. """Run checkpatch.pl on a file and parse the results.
  165. Args:
  166. fname: Filename to check
  167. verbose: True to print out every line of the checkpatch output as it is
  168. parsed
  169. show_types: Tell checkpatch to show the type (number) of each message
  170. use_tree (bool): If False we'll pass '--no-tree' to checkpatch.
  171. Returns:
  172. namedtuple containing:
  173. ok: False=failure, True=ok
  174. problems: List of problems, each a dict:
  175. 'type'; error or warning
  176. 'msg': text message
  177. 'file' : filename
  178. 'line': line number
  179. errors: Number of errors
  180. warnings: Number of warnings
  181. checks: Number of checks
  182. lines: Number of lines
  183. stdout: Full output of checkpatch
  184. """
  185. chk = find_check_patch()
  186. args = [chk]
  187. if not use_tree:
  188. args.append('--no-tree')
  189. if show_types:
  190. args.append('--show-types')
  191. output = command.output(*args, fname, raise_on_error=False)
  192. return check_patch_parse(output, verbose)
  193. def get_warning_msg(col, msg_type, fname, line, msg):
  194. '''Create a message for a given file/line
  195. Args:
  196. msg_type: Message type ('error' or 'warning')
  197. fname: Filename which reports the problem
  198. line: Line number where it was noticed
  199. msg: Message to report
  200. '''
  201. if msg_type == 'warning':
  202. msg_type = col.build(col.YELLOW, msg_type)
  203. elif msg_type == 'error':
  204. msg_type = col.build(col.RED, msg_type)
  205. elif msg_type == 'check':
  206. msg_type = col.build(col.MAGENTA, msg_type)
  207. line_str = '' if line is None else '%d' % line
  208. return '%s:%s: %s: %s\n' % (fname, line_str, msg_type, msg)
  209. def check_patches(verbose, args, use_tree):
  210. '''Run the checkpatch.pl script on each patch'''
  211. error_count, warning_count, check_count = 0, 0, 0
  212. col = terminal.Color()
  213. with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
  214. futures = []
  215. for fname in args:
  216. f = executor.submit(check_patch, fname, verbose, use_tree=use_tree)
  217. futures.append(f)
  218. for fname, f in zip(args, futures):
  219. result = f.result()
  220. if not result.ok:
  221. error_count += result.errors
  222. warning_count += result.warnings
  223. check_count += result.checks
  224. print('%d errors, %d warnings, %d checks for %s:' % (result.errors,
  225. result.warnings, result.checks, col.build(col.BLUE, fname)))
  226. if (len(result.problems) != result.errors + result.warnings +
  227. result.checks):
  228. print("Internal error: some problems lost")
  229. # Python seems to get confused by this
  230. # pylint: disable=E1133
  231. for item in result.problems:
  232. sys.stderr.write(
  233. get_warning_msg(col, item.get('type', '<unknown>'),
  234. item.get('file', '<unknown>'),
  235. item.get('line', 0), item.get('msg', 'message')))
  236. print
  237. if error_count or warning_count or check_count:
  238. str = 'checkpatch.pl found %d error(s), %d warning(s), %d checks(s)'
  239. color = col.GREEN
  240. if warning_count:
  241. color = col.YELLOW
  242. if error_count:
  243. color = col.RED
  244. print(col.build(color, str % (error_count, warning_count, check_count)))
  245. return False
  246. return True