• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2024 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"""Code formatter plugin for C/C++."""
15
16from pathlib import Path
17from typing import Final, Iterable, Iterator, Sequence
18
19from pw_cli.file_filter import FileFilter
20from pw_presubmit.format.core import (
21    FileFormatter,
22    FormattedFileContents,
23    FormatFixStatus,
24)
25
26
27CPP_HEADER_EXTS = frozenset(('.h', '.hpp', '.hxx', '.h++', '.hh', '.H'))
28CPP_SOURCE_EXTS = frozenset(
29    ('.c', '.cpp', '.cxx', '.c++', '.cc', '.C', '.inc', '.inl')
30)
31CPP_EXTS = CPP_HEADER_EXTS.union(CPP_SOURCE_EXTS)
32DEFAULT_CPP_FILE_PATTERNS = FileFilter(
33    endswith=CPP_EXTS, exclude=[r'\.pb\.h$', r'\.pb\.c$']
34)
35
36
37class ClangFormatFormatter(FileFormatter):
38    """A formatter that runs `clang-format` on files."""
39
40    DEFAULT_FLAGS: Final[Sequence[str]] = ('--style=file',)
41
42    def __init__(self, tool_flags: Sequence[str] = DEFAULT_FLAGS, **kwargs):
43        kwargs.setdefault('mnemonic', 'C and C++')
44        kwargs.setdefault('file_patterns', DEFAULT_CPP_FILE_PATTERNS)
45        super().__init__(**kwargs)
46        self.clang_format_flags = list(tool_flags)
47
48    def format_file_in_memory(
49        self, file_path: Path, file_contents: bytes
50    ) -> FormattedFileContents:
51        """Uses ``clang-format`` to check the formatting of the requested file.
52
53        The file at ``file_path`` is NOT modified by this check.
54
55        Returns:
56            A populated
57            :py:class:`pw_presubmit.format.core.FormattedFileContents` that
58            contains either the result of formatting the file, or an error
59            message.
60        """
61        proc = self.run_tool(
62            'clang-format',
63            self.clang_format_flags + [file_path],
64        )
65        return FormattedFileContents(
66            ok=proc.returncode == 0,
67            formatted_file_contents=proc.stdout,
68            error_message=proc.stderr.decode()
69            if proc.returncode != 0
70            else None,
71        )
72
73    def format_file(self, file_path: Path) -> FormatFixStatus:
74        """Formats the provided file in-place using ``clang-format``.
75
76        Returns:
77            A FormatFixStatus that contains relevant errors/warnings.
78        """
79        self.format_files([file_path])
80        # `clang-format` doesn't emit errors, and will always try its best to
81        # format malformed files.
82        return FormatFixStatus(ok=True, error_message=None)
83
84    def format_files(
85        self, paths: Iterable[Path], keep_warnings: bool = True
86    ) -> Iterator[tuple[Path, FormatFixStatus]]:
87        """Uses ``clang-format`` to format the specified files in-place.
88
89        Returns:
90            An iterator of ``Path`` and
91            :py:class:`pw_presubmit.format.core.FormatFixStatus` pairs for each
92            file that was not successfully formatted. If ``keep_warnings`` is
93            ``True``, any successful format operations with warnings will also
94            be returned.
95        """
96        self.run_tool(
97            'clang-format',
98            ['-i'] + self.clang_format_flags + list(paths),
99            check=True,
100        )
101
102        # `clang-format` doesn't emit errors, and will always try its best to
103        # format malformed files. For that reason, no errors are yielded.
104        yield from []
105