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 Bazel build files.""" 15 16from pathlib import Path 17from typing import Dict, Final, Iterable, Iterator, List, Sequence, Tuple 18 19from pw_presubmit.format.core import ( 20 FileFormatter, 21 FormattedFileContents, 22 FormatFixStatus, 23) 24 25 26class BuildifierFormatter(FileFormatter): 27 """A formatter that runs ``buildifier`` on files.""" 28 29 # These warnings are safe to enable because they can always be auto-fixed. 30 DEFAULT_WARNINGS_TO_FIX: Final[Sequence[str]] = ( 31 'load', 32 'load-on-top', 33 'native-build', 34 'same-origin-load', 35 'out-of-order-load', 36 'unsorted-dict-items', 37 ) 38 39 def __init__( 40 self, warnings_to_fix: Sequence[str] = DEFAULT_WARNINGS_TO_FIX, **kwargs 41 ): 42 super().__init__(**kwargs) 43 self.warnings_to_fix = list(warnings_to_fix) 44 45 @staticmethod 46 def _detect_file_type(file_path: Path) -> str: 47 if file_path.name == 'MODULE.bazel': 48 return 'module' 49 if file_path.name == 'WORKSPACE': 50 return 'workspace' 51 if '.bzl' in file_path.name: 52 return 'bzl' 53 if 'BUILD' in file_path.name or file_path.suffix == '.bazel': 54 return 'build' 55 56 return 'default' 57 58 def _files_by_type(self, paths: Iterable[Path]) -> Dict[str, List[Path]]: 59 all_types = ( 60 'module', 61 'workspace', 62 'bzl', 63 'build', 64 'default', 65 ) 66 all_files: Dict[str, List[Path]] = {t: [] for t in all_types} 67 68 for file_path in paths: 69 all_files[self._detect_file_type(file_path)].append(file_path) 70 71 return all_files 72 73 def format_file_in_memory( 74 self, file_path: Path, file_contents: bytes 75 ) -> FormattedFileContents: 76 """Uses ``buildifier`` to check the formatting of the requested file. 77 78 The file at ``file_path`` is NOT modified by this check. 79 80 Returns: 81 A populated 82 :py:class:`pw_presubmit.format.core.FormattedFileContents` that 83 contains either the result of formatting the file, or an error 84 message. 85 """ 86 proc = self.run_tool( 87 'buildifier', 88 [ 89 f'--type={self._detect_file_type(file_path)}', 90 '--lint=fix', 91 '--warnings=' + ','.join(self.warnings_to_fix), 92 ], 93 input=file_contents, 94 ) 95 ok = proc.returncode == 0 96 return FormattedFileContents( 97 ok=ok, 98 formatted_file_contents=proc.stdout, 99 error_message=None if ok else proc.stderr.decode(), 100 ) 101 102 def format_file(self, file_path: Path) -> FormatFixStatus: 103 """Formats the provided file in-place using ``buildifier``. 104 105 Returns: 106 A FormatFixStatus that contains relevant errors/warnings. 107 """ 108 proc = self.run_tool( 109 'buildifier', 110 [ 111 f'--type={self._detect_file_type(file_path)}', 112 '--lint=fix', 113 '--warnings=' + ','.join(self.warnings_to_fix), 114 file_path, 115 ], 116 ) 117 ok = proc.returncode == 0 118 return FormatFixStatus( 119 ok=ok, 120 error_message=None if ok else proc.stderr.decode(), 121 ) 122 123 def format_files( 124 self, paths: Iterable[Path], keep_warnings: bool = True 125 ) -> Iterator[Tuple[Path, FormatFixStatus]]: 126 """Uses ``buildifier`` to format the specified files in-place. 127 128 Returns: 129 An iterator of ``Path`` and 130 :py:class:`pw_presubmit.format.core.FormatFixStatus` pairs for each 131 file that was not successfully formatted. If ``keep_warnings`` is 132 ``True``, any successful format operations with warnings will also 133 be returned. 134 """ 135 sorted_files = self._files_by_type(paths) 136 for file_type, type_specific_paths in sorted_files.items(): 137 if not type_specific_paths: 138 continue 139 140 proc = self.run_tool( 141 'buildifier', 142 [ 143 f'--type={file_type}', 144 '--lint=fix', 145 '--warnings=' + ','.join(self.warnings_to_fix), 146 *type_specific_paths, 147 ], 148 ) 149 150 # If there's an error, fall back to per-file formatting to figure 151 # out which file has problems. 152 if proc.returncode != 0: 153 yield from super().format_files( 154 type_specific_paths, keep_warnings 155 ) 156