• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.dev/issues/\d+ - )|'
49    r'(?:\bTODO: https://pwbug.dev/\d+ - )|'
50    r'(?:\bTODO: pwbug.dev/\d+ - )|'
51    r'(?:\bTODO: <pwbug.dev/\d+> - )|'
52    r'(?:\bTODO: https://github\.com/bazelbuild/[a-z][-_a-z0-9]*/issues/\d+[ ]-[ ])'
53)
54BUGS_OR_USERNAMES = re.compile(
55    r"""
56(?:  # Legacy style.
57    \bTODO\(
58        (?:b/\d+|[a-z]+)  # Username or bug.
59        (?:,[ ]?(?:b/\d+|[a-z]+))*  # Additional usernames or bugs.
60    \)
61.*\w  # Explanation.
62)|
63(?:  # New style.
64    \bTODO:[ ]
65    (?:
66        b/\d+|  # Bug.
67        https://pwbug.dev/\d+| # Short URL
68        pwbug.dev/\d+| # Even shorter URL
69        <pwbug.dev/\d+>| # Markdown compatible even shorter URL
70        https://issues.pigweed.dev/issues/\d+| # Fully qualified bug for rustdoc
71        # Username@ with optional domain.
72        [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
73    )
74    (?:,[ ]?  # Additional.
75        (?:
76            b/\d+|  # Bug.
77            # Username@ with optional domain.
78            [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
79        )
80    )*
81[ ]-[ ].*\w  # Explanation.
82)|
83(?:  # Fuchsia style.
84    \bTODO\(
85        (?:fxbug\.dev/\d+|[a-z]+)  # Username or bug.
86        (?:,[ ]?(?:fxbug\.dev/\d+|[a-z]+))*  # Additional usernames or bugs.
87    \)
88.*\w  # Explanation.
89)|
90(?:  # Bazel GitHub issues. No usernames allowed.
91    \bTODO:[ ]
92    (?:
93        https://github\.com/bazelbuild/[a-z][-_a-z0-9]*/issues/\d+
94    )
95[ ]-[ ].*\w  # Explanation.
96)
97    """,
98    re.VERBOSE,
99)
100# pylint: enable=line-too-long
101
102_TODO_OR_FIXME = re.compile(r'(\bTODO\b)|(\bFIXME\b)')
103# todo-check: enable
104
105# If seen, ignore this line and the next.
106_IGNORE = 'todo-check: ignore'
107
108# Ignore a whole section. Please do not change the order of these lines.
109_DISABLE = 'todo-check: disable'
110_ENABLE = 'todo-check: enable'
111
112
113def _process_file(ctx: PresubmitContext, todo_pattern: re.Pattern, path: Path):
114    with path.open() as ins:
115        _LOG.debug('Evaluating path %s', path)
116        enabled = True
117        prev = ''
118
119        try:
120            summary: list[str] = []
121            for i, line in enumerate(ins, 1):
122                if _DISABLE in line:
123                    enabled = False
124                elif _ENABLE in line:
125                    enabled = True
126
127                if not enabled or _IGNORE in line or _IGNORE in prev:
128                    prev = line
129                    continue
130
131                if _TODO_OR_FIXME.search(line):
132                    if not todo_pattern.search(line):
133                        # todo-check: ignore
134                        ctx.fail(f'Bad TODO on line {i}:', path)
135                        ctx.fail(f'    {line.strip()}')
136                        ctx.fail('Prefer this format in new code:')
137                        # todo-check: ignore
138                        ctx.fail(
139                            '    TODO: https://pwbug.dev/12345 - More context.'
140                        )
141                        summary.append(f'{i}:{line.strip()}')
142
143                prev = line
144
145            return summary
146
147        except UnicodeDecodeError:
148            # File is not text, like a gif.
149            _LOG.debug('File %s is not a text file', path)
150            return []
151
152
153def create(
154    todo_pattern: re.Pattern = BUGS_ONLY,
155    exclude: Iterable[Pattern[str] | str] = EXCLUDE,
156):
157    """Create a todo_check presubmit step that uses the given pattern."""
158
159    @filter_paths(exclude=exclude)
160    def todo_check(ctx: PresubmitContext):
161        """Check that TODO lines are valid."""  # todo-check: ignore
162        ctx.paths = presubmit_context.apply_exclusions(ctx)
163        summary: dict[Path, list[str]] = {}
164        for path in ctx.paths:
165            if file_summary := _process_file(ctx, todo_pattern, path):
166                summary[path] = file_summary
167
168        if summary:
169            with ctx.failure_summary_log.open('w') as outs:
170                for path, lines in summary.items():
171                    print('====', path.relative_to(ctx.root), file=outs)
172                    for line in lines:
173                        print(line, file=outs)
174
175    return todo_check
176