xref: /aosp_15_r20/external/pigweed/pw_presubmit/py/pw_presubmit/cpp_checks.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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