xref: /aosp_15_r20/external/pigweed/pw_presubmit/py/pw_presubmit/todo_check.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Check the formatting of TODOs."""
15
16import logging
17from pathlib import Path
18import re
19from typing import Iterable, Pattern, Sequence
20
21from pw_presubmit import presubmit_context
22from pw_presubmit.presubmit import filter_paths
23from pw_presubmit.presubmit_context import PresubmitContext
24
25_LOG: logging.Logger = logging.getLogger(__name__)
26
27EXCLUDE: Sequence[str] = (
28    # Metadata
29    r'^docker/tag$',
30    r'\byarn.lock$',
31    # Data files
32    r'\.bin$',
33    r'\.csv$',
34    r'\.elf$',
35    r'\.gif$',
36    r'\.jpg$',
37    r'\.json$',
38    r'\.png$',
39    r'\.svg$',
40    r'\.xml$',
41)
42
43# todo-check: disable
44# pylint: disable=line-too-long
45BUGS_ONLY = re.compile(
46    r'(?:\bTODO\(b/\d+(?:, ?b/\d+)*\).*\w)|'
47    r'(?:\bTODO: b/\d+(?:, ?b/\d+)* - )|'
48    r'(?:\bTODO: https://issues\.(?:pigweed|fuchsia)\.dev/(?:issues/)?\d+ - )|'
49    r'(?:\bTODO: https://issues\.chromium\.org/(?:issues/)?\d+ - )|'
50    r'(?:\bTODO: https://(?:(?:pw|fx)bug\.dev|crbug\.com)/\d+ - )|'
51    r'(?:\bTODO: (?:(?:pw|fx)bug\.dev|crbug\.com)/\d+ - )|'
52    r'(?:\bTODO: <(?:(?:pw|fx)bug\.dev|crbug\.com)/\d+> - )|'
53    r'(?:\bTODO: https://github\.com/bazelbuild/[a-z][-_a-z0-9]*/issues/\d+[ ]-[ ])'
54)
55BUGS_OR_USERNAMES = re.compile(
56    r"""
57(?:  # Legacy style.
58    \bTODO\(
59        (?:b/\d+|[a-z]+)  # Username or bug.
60        (?:,[ ]?(?:b/\d+|[a-z]+))*  # Additional usernames or bugs.
61    \)
62.*\w  # Explanation.
63)|
64(?:  # New style.
65    \bTODO:[ ]
66    (?:
67        b/\d+|  # Bug.
68        https://(?:(?:pw|fx)bug\.dev|crbug\.com)(?:/issues)?/\d+| # Short URL
69        (?:(?:pw|fx)bug\.dev|crbug\.com)(?:/issues)?/\d+| # Even shorter URL
70        <(?:(?:pw|fx)bug\.dev|crbug\.com)/\d+>| # Markdown compatible
71        https://issues\.(?:pigweed|fuchsia)\.dev(?:/issues)?/\d+| # Fully qualified bug for rustdoc
72        https://issues.chromium.org/issues/\d+| # Fully qualified bug for rustdoc
73        # Username@ with optional domain.
74        [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
75    )
76    (?:,[ ]?  # Additional.
77        (?:
78            b/\d+|  # Bug.
79            # Username@ with optional domain.
80            [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
81        )
82    )*
83[ ]-[ ].*\w  # Explanation.
84)|
85(?:  # Fuchsia style.
86    \bTODO\(
87        (?:fxbug\.dev/\d+|[a-z]+)  # Username or bug.
88        (?:,[ ]?(?:fxbug\.dev/\d+|[a-z]+))*  # Additional usernames or bugs.
89    \)
90.*\w  # Explanation.
91)|
92(?:  # Bazel GitHub issues. No usernames allowed.
93    \bTODO:[ ]
94    (?:
95        https://github\.com/bazelbuild/[a-z][-_a-z0-9]*/issues/\d+
96    )
97[ ]-[ ].*\w  # Explanation.
98)
99    """,
100    re.VERBOSE,
101)
102# pylint: enable=line-too-long
103
104_TODO_OR_FIXME = re.compile(r'(\bTODO\b)|(\bFIXME\b)')
105# todo-check: enable
106
107# If seen, ignore this line and the next.
108_IGNORE = 'todo-check: ignore'
109
110# Ignore a whole section. Please do not change the order of these lines.
111_DISABLE = 'todo-check: disable'
112_ENABLE = 'todo-check: enable'
113
114
115def _process_file(ctx: PresubmitContext, todo_pattern: re.Pattern, path: Path):
116    with path.open() as ins:
117        _LOG.debug('Evaluating path %s', path)
118        enabled = True
119        prev = ''
120
121        try:
122            summary: list[str] = []
123            for i, line in enumerate(ins, 1):
124                if _DISABLE in line:
125                    enabled = False
126                elif _ENABLE in line:
127                    enabled = True
128
129                if not enabled or _IGNORE in line or _IGNORE in prev:
130                    prev = line
131                    continue
132
133                if _TODO_OR_FIXME.search(line):
134                    if not todo_pattern.search(line):
135                        # todo-check: ignore
136                        ctx.fail(f'Bad TODO on line {i}:', path)
137                        ctx.fail(f'    {line.strip()}')
138                        ctx.fail('Prefer this format in new code:')
139                        # todo-check: ignore
140                        ctx.fail(
141                            '    TODO: https://pwbug.dev/12345 - More context.'
142                        )
143                        summary.append(f'{i}:{line.strip()}')
144
145                prev = line
146
147            return summary
148
149        except UnicodeDecodeError:
150            # File is not text, like a gif.
151            _LOG.debug('File %s is not a text file', path)
152            return []
153
154
155def create(
156    todo_pattern: re.Pattern = BUGS_ONLY,
157    exclude: Iterable[Pattern[str] | str] = EXCLUDE,
158):
159    """Create a todo_check presubmit step that uses the given pattern."""
160
161    @filter_paths(exclude=exclude)
162    def todo_check(ctx: PresubmitContext):
163        """Check that TODO lines are valid."""  # todo-check: ignore
164        ctx.paths = presubmit_context.apply_exclusions(ctx)
165        summary: dict[Path, list[str]] = {}
166        for path in ctx.paths:
167            if file_summary := _process_file(ctx, todo_pattern, path):
168                summary[path] = file_summary
169
170        if summary:
171            with ctx.failure_summary_log.open('w') as outs:
172                for path, lines in summary.items():
173                    print('====', path.relative_to(ctx.root), file=outs)
174                    for line in lines:
175                        print(line, file=outs)
176
177    return todo_check
178