aboutsummaryrefslogtreecommitdiff
path: root/pw_presubmit/py/pw_presubmit/cpp_checks.py
blob: a86cb092cafd111c5e8a0f6603bb57ee1be81ad1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""C++-related checks."""

import logging
from pathlib import Path
import re
from typing import Callable, Optional, Iterable, Iterator

from pw_presubmit.presubmit import (
    Check,
    filter_paths,
)
from pw_presubmit.presubmit_context import PresubmitContext
from pw_presubmit import (
    build,
    format_code,
    presubmit_context,
)

_LOG: logging.Logger = logging.getLogger(__name__)


def _fail(ctx, error, path):
    ctx.fail(error, path=path)
    with open(ctx.failure_summary_log, 'a') as outs:
        print(f'{path}\n{error}\n', file=outs)


@filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',))
def pragma_once(ctx: PresubmitContext) -> None:
    """Presubmit check that ensures all header files contain '#pragma once'."""

    ctx.paths = presubmit_context.apply_exclusions(ctx)

    for path in ctx.paths:
        _LOG.debug('Checking %s', path)
        with path.open() as file:
            for line in file:
                if line.startswith('#pragma once'):
                    break
            else:
                _fail(ctx, '#pragma once is missing!', path=path)


def include_guard_check(
    guard: Optional[Callable[[Path], str]] = None,
    allow_pragma_once: bool = True,
) -> Check:
    """Create an include guard check.

    Args:
        guard: Callable that generates an expected include guard name for the
            given Path. If None, any include guard is acceptable, as long as
            it's consistent between the '#ifndef' and '#define' lines.
        allow_pragma_once: Whether to allow headers to use '#pragma once'
            instead of '#ifndef'/'#define'.
    """

    def stripped_non_comment_lines(iterable: Iterable[str]) -> Iterator[str]:
        """Yield non-comment non-empty lines from a C++ file."""
        multi_line_comment = False
        for line in iterable:
            line = line.strip()
            if not line:
                continue
            if line.startswith('//'):
                continue
            if line.startswith('/*'):
                multi_line_comment = True
            if multi_line_comment:
                if line.endswith('*/'):
                    multi_line_comment = False
                continue
            yield line

    def check_path(ctx: PresubmitContext, path: Path) -> None:
        """Check if path has a valid include guard."""

        _LOG.debug('checking %s', path)
        expected: Optional[str] = None
        if guard:
            expected = guard(path)
            _LOG.debug('expecting guard %r', expected)

        with path.open() as ins:
            iterable = stripped_non_comment_lines(ins)
            first_line = next(iterable, '')
            _LOG.debug('first line %r', first_line)

            if allow_pragma_once and first_line.startswith('#pragma once'):
                _LOG.debug('found %r', first_line)
                return

            if expected:
                ifndef_expected = f'#ifndef {expected}'
                if not re.match(rf'^#ifndef {expected}$', first_line):
                    _fail(
                        ctx,
                        'Include guard is missing! Expected: '
                        f'{ifndef_expected!r}, Found: {first_line!r}',
                        path=path,
                    )
                    return

            else:
                match = re.match(
                    r'^#\s*ifndef\s+([a-zA-Z_][a-zA-Z_0-9]*)$',
                    first_line,
                )
                if not match:
                    _fail(
                        ctx,
                        'Include guard is missing! Expected "#ifndef" line, '
                        f'Found: {first_line!r}',
                        path=path,
                    )
                    return
                expected = match.group(1)

            second_line = next(iterable, '')
            _LOG.debug('second line %r', second_line)

            if not re.match(rf'^#\s*define\s+{expected}$', second_line):
                define_expected = f'#define {expected}'
                _fail(
                    ctx,
                    'Include guard is missing! Expected: '
                    f'{define_expected!r}, Found: {second_line!r}',
                    path=path,
                )
                return

            _LOG.debug('passed')

    @filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',))
    def include_guard(ctx: PresubmitContext):
        """Check that all header files contain an include guard."""
        ctx.paths = presubmit_context.apply_exclusions(ctx)
        for path in ctx.paths:
            check_path(ctx, path)

    return include_guard


@Check
def asan(ctx: PresubmitContext) -> None:
    """Test with the address sanitizer."""
    build.gn_gen(ctx)
    build.ninja(ctx, 'asan')


@Check
def msan(ctx: PresubmitContext) -> None:
    """Test with the memory sanitizer."""
    build.gn_gen(ctx)
    build.ninja(ctx, 'msan')


@Check
def tsan(ctx: PresubmitContext) -> None:
    """Test with the thread sanitizer."""
    build.gn_gen(ctx)
    build.ninja(ctx, 'tsan')


@Check
def ubsan(ctx: PresubmitContext) -> None:
    """Test with the undefined behavior sanitizer."""
    build.gn_gen(ctx)
    build.ninja(ctx, 'ubsan')


@Check
def runtime_sanitizers(ctx: PresubmitContext) -> None:
    """Test with the address, thread, and undefined behavior sanitizers."""
    build.gn_gen(ctx)
    build.ninja(ctx, 'runtime_sanitizers')


def all_sanitizers():
    # TODO: b/234876100 - msan will not work until the C++ standard library
    # included in the sysroot has a variant built with msan.
    return [asan, tsan, ubsan, runtime_sanitizers]