aboutsummaryrefslogtreecommitdiff
path: root/pw_presubmit/py/pw_presubmit/cpp_checks.py
diff options
context:
space:
mode:
Diffstat (limited to 'pw_presubmit/py/pw_presubmit/cpp_checks.py')
-rw-r--r--pw_presubmit/py/pw_presubmit/cpp_checks.py126
1 files changed, 120 insertions, 6 deletions
diff --git a/pw_presubmit/py/pw_presubmit/cpp_checks.py b/pw_presubmit/py/pw_presubmit/cpp_checks.py
index 849242455..a86cb092c 100644
--- a/pw_presubmit/py/pw_presubmit/cpp_checks.py
+++ b/pw_presubmit/py/pw_presubmit/cpp_checks.py
@@ -14,30 +14,144 @@
"""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,
- Check,
format_code,
- PresubmitContext,
- filter_paths,
+ 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 open(path) as file:
+ with path.open() as file:
for line in file:
if line.startswith('#pragma once'):
break
else:
- ctx.fail('#pragma once is missing!', path=path)
+ _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
@@ -76,6 +190,6 @@ def runtime_sanitizers(ctx: PresubmitContext) -> None:
def all_sanitizers():
- # TODO(b/234876100): msan will not work until the C++ standard library
+ # 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]