1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2011 The Chromium OS Authors.
3#
4
5import collections
6import os
7import re
8import sys
9
10from patman import command
11from patman import gitutil
12from patman import terminal
13
14EMACS_PREFIX = r'(?:[0-9]{4}.*\.patch:[0-9]+: )?'
15TYPE_NAME = r'([A-Z_]+:)?'
16RE_ERROR = re.compile(r'ERROR:%s (.*)' % TYPE_NAME)
17RE_WARNING = re.compile(EMACS_PREFIX + r'WARNING:%s (.*)' % TYPE_NAME)
18RE_CHECK = re.compile(r'CHECK:%s (.*)' % TYPE_NAME)
19RE_FILE = re.compile(r'#(\d+): (FILE: ([^:]*):(\d+):)?')
20RE_NOTE = re.compile(r'NOTE: (.*)')
21
22
23def FindCheckPatch():
24    top_level = gitutil.GetTopLevel()
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
50def CheckPatchParseOneMessage(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
117def CheckPatchParse(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 = CheckPatchParseOneMessage(message)
183            if problem:
184                result.problems.append(problem)
185
186    return result
187
188
189def CheckPatch(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 = FindCheckPatch()
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 CheckPatchParse(output, verbose)
219
220
221def GetWarningMsg(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.Color(col.YELLOW, msg_type)
232    elif msg_type == 'error':
233        msg_type = col.Color(col.RED, msg_type)
234    elif msg_type == 'check':
235        msg_type = col.Color(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
239def CheckPatches(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 = CheckPatch(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.Color(col.BLUE, fname)))
252            if (len(result.problems) != result.errors + result.warnings +
253                    result.checks):
254                print("Internal error: some problems lost")
255            for item in result.problems:
256                sys.stderr.write(
257                    GetWarningMsg(col, item.get('type', '<unknown>'),
258                        item.get('file', '<unknown>'),
259                        item.get('line', 0), item.get('msg', 'message')))
260            print
261            #print(stdout)
262    if error_count or warning_count or check_count:
263        str = 'checkpatch.pl found %d error(s), %d warning(s), %d checks(s)'
264        color = col.GREEN
265        if warning_count:
266            color = col.YELLOW
267        if error_count:
268            color = col.RED
269        print(col.Color(color, str % (error_count, warning_count, check_count)))
270        return False
271    return True
272