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