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