• 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 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