1# Copyright 2021 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"""C++-related checks.""" 15 16import logging 17from pathlib import Path 18import re 19from typing import Callable, Iterable, Iterator 20 21from pw_presubmit.presubmit import ( 22 Check, 23 filter_paths, 24) 25from pw_presubmit.presubmit_context import PresubmitContext 26from pw_presubmit import ( 27 build, 28 format_code, 29 presubmit_context, 30) 31 32_LOG: logging.Logger = logging.getLogger(__name__) 33 34 35def _fail(ctx, error, path): 36 ctx.fail(error, path=path) 37 with open(ctx.failure_summary_log, 'a') as outs: 38 print(f'{path}\n{error}\n', file=outs) 39 40 41@filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',)) 42def pragma_once(ctx: PresubmitContext) -> None: 43 """Presubmit check that ensures all header files contain '#pragma once'.""" 44 45 ctx.paths = presubmit_context.apply_exclusions(ctx) 46 47 for path in ctx.paths: 48 _LOG.debug('Checking %s', path) 49 with path.open() as file: 50 for line in file: 51 if line.startswith('#pragma once'): 52 break 53 else: 54 _fail(ctx, '#pragma once is missing!', path=path) 55 56 57def include_guard_check( 58 guard: Callable[[Path], str] | None = None, 59 allow_pragma_once: bool = True, 60) -> Check: 61 """Create an include guard check. 62 63 Args: 64 guard: Callable that generates an expected include guard name for the 65 given Path. If None, any include guard is acceptable, as long as 66 it's consistent between the '#ifndef' and '#define' lines. 67 allow_pragma_once: Whether to allow headers to use '#pragma once' 68 instead of '#ifndef'/'#define'. 69 """ 70 71 def stripped_non_comment_lines(iterable: Iterable[str]) -> Iterator[str]: 72 """Yield non-comment non-empty lines from a C++ file.""" 73 multi_line_comment = False 74 for line in iterable: 75 line = line.strip() 76 if not line: 77 continue 78 if line.startswith('//'): 79 continue 80 if line.startswith('/*'): 81 multi_line_comment = True 82 if multi_line_comment: 83 if line.endswith('*/'): 84 multi_line_comment = False 85 continue 86 yield line 87 88 def check_path(ctx: PresubmitContext, path: Path) -> None: 89 """Check if path has a valid include guard.""" 90 91 _LOG.debug('checking %s', path) 92 expected: str | None = None 93 if guard: 94 expected = guard(path) 95 _LOG.debug('expecting guard %r', expected) 96 97 with path.open() as ins: 98 iterable = stripped_non_comment_lines(ins) 99 first_line = next(iterable, '') 100 _LOG.debug('first line %r', first_line) 101 102 if allow_pragma_once and first_line.startswith('#pragma once'): 103 _LOG.debug('found %r', first_line) 104 return 105 106 if expected: 107 ifndef_expected = f'#ifndef {expected}' 108 if not re.match(rf'^#ifndef {expected}$', first_line): 109 _fail( 110 ctx, 111 'Include guard is missing! Expected: ' 112 f'{ifndef_expected!r}, Found: {first_line!r}', 113 path=path, 114 ) 115 return 116 117 else: 118 match = re.match( 119 r'^#\s*ifndef\s+([a-zA-Z_][a-zA-Z_0-9]*)$', 120 first_line, 121 ) 122 if not match: 123 _fail( 124 ctx, 125 'Include guard is missing! Expected "#ifndef" line, ' 126 f'Found: {first_line!r}', 127 path=path, 128 ) 129 return 130 expected = match.group(1) 131 132 second_line = next(iterable, '') 133 _LOG.debug('second line %r', second_line) 134 135 if not re.match(rf'^#\s*define\s+{expected}$', second_line): 136 define_expected = f'#define {expected}' 137 _fail( 138 ctx, 139 'Include guard is missing! Expected: ' 140 f'{define_expected!r}, Found: {second_line!r}', 141 path=path, 142 ) 143 return 144 145 _LOG.debug('passed') 146 147 @filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',)) 148 def include_guard(ctx: PresubmitContext): 149 """Check that all header files contain an include guard.""" 150 ctx.paths = presubmit_context.apply_exclusions(ctx) 151 for path in ctx.paths: 152 check_path(ctx, path) 153 154 return include_guard 155 156 157@Check 158def asan(ctx: PresubmitContext) -> None: 159 """Test with the address sanitizer.""" 160 build.gn_gen(ctx) 161 build.ninja(ctx, 'asan') 162 163 164@Check 165def msan(ctx: PresubmitContext) -> None: 166 """Test with the memory sanitizer.""" 167 build.gn_gen(ctx) 168 build.ninja(ctx, 'msan') 169 170 171@Check 172def tsan(ctx: PresubmitContext) -> None: 173 """Test with the thread sanitizer.""" 174 build.gn_gen(ctx) 175 build.ninja(ctx, 'tsan') 176 177 178@Check 179def ubsan(ctx: PresubmitContext) -> None: 180 """Test with the undefined behavior sanitizer.""" 181 build.gn_gen(ctx) 182 build.ninja(ctx, 'ubsan') 183 184 185@Check 186def runtime_sanitizers(ctx: PresubmitContext) -> None: 187 """Test with the address, thread, and undefined behavior sanitizers.""" 188 build.gn_gen(ctx) 189 build.ninja(ctx, 'runtime_sanitizers') 190 191 192def all_sanitizers(): 193 # TODO: b/234876100 - msan will not work until the C++ standard library 194 # included in the sysroot has a variant built with msan. 195 return [asan, tsan, ubsan, runtime_sanitizers] 196