• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2025 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"""A CLI utility that checks and fixes formatting for source files."""
15
16import argparse
17import itertools
18import logging
19import os
20from pathlib import Path
21import sys
22from typing import Collection, Pattern
23
24from pw_cli import log
25from pw_cli.collect_files import (
26    collect_files_in_current_repo,
27    file_summary,
28)
29from pw_cli.plural import plural
30from pw_cli.tool_runner import BasicSubprocessRunner
31from pw_presubmit.format.core import FileFormatter
32from pw_presubmit.format.private import cli_support
33
34_LOG: logging.Logger = logging.getLogger(__name__)
35
36# Only use for Git, we need to use the system git 100% of the time.
37_git_runner = BasicSubprocessRunner()
38
39
40# Eventually, this should be public. It's currently private to prevent too many
41# external dependencies while it's under development.
42class FormattingSuite:
43    """A suite of code formatters that may be run on a set of files."""
44
45    def __init__(
46        self,
47        formatters: Collection[FileFormatter],
48    ):
49        self._formatters = formatters
50
51    def main(self) -> int:
52        """Entry point for the formatter CLI."""
53        # Set up logging.
54        log.install(logging.INFO)
55
56        if 'BUILD_WORKING_DIRECTORY' in os.environ:
57            os.chdir(os.environ['BUILD_WORKING_DIRECTORY'])
58        parser = argparse.ArgumentParser(description=__doc__)
59        cli_support.add_arguments(parser)
60        args = parser.parse_args()
61        return 0 if self.format_files(**vars(args)) else 1
62
63    def format_files(
64        self,
65        paths: Collection[str | Path],
66        base: str | None,
67        exclude: Collection[Pattern] = tuple(),
68        apply_fixes: bool = True,
69    ) -> bool:
70        """Formats files in a repository.
71
72        Args:
73            paths: File paths and pathspects to format.
74            base: Filters paths to only include files modified since this
75                specified Git ref.
76            exclude: Regex patterns to exclude from the set of collected files.
77            apply_fixes: Whether or not to apply formatting fixes to files.
78
79        Returns:
80            True if operation was successful.
81        """
82        all_files = collect_files_in_current_repo(
83            paths,
84            _git_runner,
85            modified_since_git_ref=base,
86            exclude_patterns=exclude,
87            action_flavor_text='Formatting',
88        )
89
90        _LOG.info('Checking formatting for %s', plural(all_files, 'file'))
91
92        for line in file_summary(
93            cli_support.relativize_paths(all_files, Path.cwd())
94        ):
95            print(line, file=sys.stderr)
96
97        all_files = tuple(cli_support.filter_exclusions(all_files))
98
99        files_by_formatter = cli_support.map_files_to_formatters(
100            all_files, self._formatters
101        )
102
103        findings = cli_support.check(files_by_formatter)
104        findings_as_list = list(
105            itertools.chain.from_iterable(findings.values())
106        )
107
108        cli_support.summarize_findings(
109            findings_as_list,
110            log_fix_command=(not apply_fixes),
111            log_oneliner_summary=True,
112        )
113
114        if not findings:
115            _LOG.info('Congratulations! No formatting changes needed')
116            return True
117
118        if not apply_fixes:
119            _LOG.error('Formatting errors found')
120            return False
121
122        _LOG.info(
123            'Applying formatting fixes to %s',
124            plural(findings_as_list, 'file'),
125        )
126        fix_errors = cli_support.fix(findings)
127        if fix_errors:
128            _LOG.error('Failed to apply all formatting fixes')
129            return False
130
131        _LOG.info('Formatting fixes applied successfully')
132        return True
133