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 GN build files.""" 15 16from pathlib import Path 17from typing import Final, Iterable, Iterator, Sequence, Tuple 18 19from pw_presubmit.format.core import ( 20 FileFormatter, 21 FormattedFileContents, 22 FormatFixStatus, 23) 24 25 26class GnFormatter(FileFormatter): 27 """A formatter that runs `gn format` on files.""" 28 29 DEFAULT_FLAGS: Final[Sequence[str]] = () 30 31 def __init__(self, tool_flags: Sequence[str] = DEFAULT_FLAGS, **kwargs): 32 super().__init__(**kwargs) 33 self.gn_format_flags = list(tool_flags) 34 35 def format_file_in_memory( 36 self, file_path: Path, file_contents: bytes 37 ) -> FormattedFileContents: 38 """Uses ``gn format`` to check the formatting of the requested file. 39 40 The file at ``file_path`` is NOT modified by this check. 41 42 Returns: 43 A populated 44 :py:class:`pw_presubmit.format.core.FormattedFileContents` that 45 contains either the result of formatting the file, or an error 46 message. 47 """ 48 # `gn format --dry-run` only signals which files need to be updated, 49 # not what the effects are. To get the formatted result, we pipe the 50 # file in via stdin and then return the result. 51 proc = self.run_tool( 52 'gn', 53 ['format'] + self.gn_format_flags + ['--stdin'], 54 input=file_contents, 55 ) 56 ok = proc.returncode == 0 57 return FormattedFileContents( 58 ok=ok, 59 formatted_file_contents=proc.stdout if ok else b'', 60 # `gn format` emits errors over stdout, not stderr. 61 error_message=None if ok else proc.stdout.decode(), 62 ) 63 64 def format_file(self, file_path: Path) -> FormatFixStatus: 65 """Formats the provided file in-place using ``gn format``. 66 67 Returns: 68 A FormatFixStatus that contains relevant errors/warnings. 69 """ 70 proc = self.run_tool( 71 'gn', 72 ['format'] + self.gn_format_flags + [file_path], 73 ) 74 ok = proc.returncode == 0 75 return FormatFixStatus( 76 ok=ok, 77 # `gn format` emits errors over stdout, not stderr. 78 error_message=None if ok else proc.stdout.decode(), 79 ) 80 81 def format_files( 82 self, paths: Iterable[Path], keep_warnings: bool = True 83 ) -> Iterator[Tuple[Path, FormatFixStatus]]: 84 """Uses ``gn format`` to fix formatting of the specified files in-place. 85 86 Returns: 87 An iterator of ``Path`` and 88 :py:class:`pw_presubmit.format.core.FormatFixStatus` pairs for each 89 file that was not successfully formatted. If ``keep_warnings`` is 90 ``True``, any successful format operations with warnings will also 91 be returned. 92 """ 93 # Try to format all files in one `gn` command. 94 proc = self.run_tool( 95 'gn', 96 ['format'] + self.gn_format_flags + list(paths), 97 ) 98 99 # If there's an error, fall back to per-file formatting to figure out 100 # which file has problems. 101 if proc.returncode != 0: 102 yield from super().format_files(paths, keep_warnings) 103