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