pw_presubmit: Handle Bazel syntax errors
Gracefully handle syntax errors in Bazel build files:
* Catch that a syntax error was found instead of exposing the exception,
* Replace the temp file path with the original file path in the error
output, and
* Add syntax errors to the errors returned by check_bazel_format()
Also, extend 'pw format' to handle when '--fix' doesn't work as expected
and not report "Formatting fixes applied successfully" unconditionally.
Change-Id: I2ea6f706b84a819e64eefebb34984431c86baee7
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/78840
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 859cc4d..1cd3d7d 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -111,9 +111,10 @@
return _check_files(files, lambda path, _: _clang_format(path))
-def clang_format_fix(files: Iterable) -> None:
+def clang_format_fix(files: Iterable) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
_clang_format('-i', *files)
+ return {}
def check_gn_format(files: Iterable[Path]) -> Dict[Path, str]:
@@ -125,13 +126,16 @@
check=True).stdout)
-def fix_gn_format(files: Iterable[Path]) -> None:
+def fix_gn_format(files: Iterable[Path]) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
log_run(['gn', 'format', *files], check=True)
+ return {}
def check_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
+ errors: Dict[Path, str] = {}
+
def _format_temp(path: Union[Path, str], data: bytes) -> bytes:
# buildifier doesn't have an option to output the changed file, so
# copy the file to a temp location, run buildifier on it, read that
@@ -139,15 +143,27 @@
with tempfile.TemporaryDirectory() as temp:
build = Path(temp) / os.path.basename(path)
build.write_bytes(data)
- log_run(['buildifier', build], check=True)
+
+ proc = log_run(['buildifier', build], capture_output=True)
+ if proc.returncode:
+ stderr = proc.stderr.decode(errors='replace')
+ stderr = stderr.replace(str(build), str(path))
+ errors[Path(path)] = stderr
return build.read_bytes()
- return _check_files(files, _format_temp)
+ result = _check_files(files, _format_temp)
+ result.update(errors)
+ return result
-def fix_bazel_format(files: Iterable[Path]) -> None:
+def fix_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
- log_run(['buildifier', *files], check=True)
+ errors = {}
+ for path in files:
+ proc = log_run(['buildifier', path], capture_output=True)
+ if proc.returncode:
+ errors[path] = proc.stderr.decode()
+ return errors
def check_go_format(files: Iterable[Path]) -> Dict[Path, str]:
@@ -157,9 +173,10 @@
['gofmt', path], stdout=subprocess.PIPE, check=True).stdout)
-def fix_go_format(files: Iterable[Path]) -> None:
+def fix_go_format(files: Iterable[Path]) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
log_run(['gofmt', '-w', *files], check=True)
+ return {}
def _yapf(*args, **kwargs) -> subprocess.CompletedProcess:
@@ -193,9 +210,10 @@
return errors
-def fix_py_format(files: Iterable):
+def fix_py_format(files: Iterable) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
_yapf('--in-place', *files, check=True)
+ return {}
_TRAILING_SPACE = re.compile(rb'[ \t]+$', flags=re.MULTILINE)
@@ -224,8 +242,9 @@
return _check_trailing_space(files, fix=False)
-def fix_trailing_space(files: Iterable[Path]) -> None:
+def fix_trailing_space(files: Iterable[Path]) -> Dict[Path, str]:
_check_trailing_space(files, fix=True)
+ return {}
def print_format_check(errors: Dict[Path, str],
@@ -260,7 +279,7 @@
extensions: Collection[str]
exclude: Collection[str]
check: Callable[[Iterable], Dict[Path, str]]
- fix: Callable[[Iterable], None]
+ fix: Callable[[Iterable], Dict[Path, str]]
CPP_HEADER_EXTS = frozenset(
@@ -371,12 +390,22 @@
return collections.OrderedDict(sorted(errors.items()))
- def fix(self) -> None:
+ def fix(self) -> Dict[Path, str]:
"""Fixes format errors for supported files in place."""
+ all_errors: Dict[Path, str] = {}
for code_format, files in self._formats.items():
- code_format.fix(files)
+ errors = code_format.fix(files)
+ if errors:
+ for path, error in errors.items():
+ _LOG.error('Failed to format %s', path)
+ for line in error.splitlines():
+ _LOG.error('%s', line)
+ all_errors.update(errors)
+ continue
+
_LOG.info('Formatted %s',
plural(files, code_format.language + ' file'))
+ return all_errors
def _file_summary(files: Iterable[Union[Path, str]], base: Path) -> List[str]:
@@ -423,13 +452,19 @@
for line in _file_summary(paths, repo if repo else Path.cwd()):
print(line, file=sys.stderr)
- errors = formatter.check()
- print_format_check(errors, show_fix_commands=(not fix))
+ check_errors = formatter.check()
+ print_format_check(check_errors, show_fix_commands=(not fix))
- if errors:
+ if check_errors:
if fix:
- formatter.fix()
- # TODO: This should perhaps check that the fixes were successful.
+ _LOG.info('Applying formatting fixes to %d files',
+ len(check_errors))
+ fix_errors = formatter.fix()
+ if fix_errors:
+ _LOG.info('Failed to apply formatting fixes')
+ print_format_check(fix_errors, show_fix_commands=False)
+ return 1
+
_LOG.info('Formatting fixes applied successfully')
return 0