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