From cb7e834620f066942418e1beb952f3806ea4594c Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sat, 29 Aug 2026 04:41:01 +0000 Subject: [PATCH] Treat black's NothingChanged as a successful no-op black reports "this is already formatted" by raising NothingChanged rather than by returning the content unchanged. BlackFormatter did not catch it, so whenever an autofix happened to produce content that black had nothing to say about, the exception escaped format_module. fixit_bytes wraps the whole lint-and-fix pass in a broad except, so the result was worse than a lost fix: the run reported a lint error carrying black.report.NothingChanged, and the fix was discarded. The ufmt formatter is unaffected because ufmt_bytes handles this internally, which is why the issue only shows up with formatter = "black". Closes #427 --- src/fixit/format.py | 13 ++++++++++--- src/fixit/tests/__init__.py | 1 + src/fixit/tests/format.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/fixit/tests/format.py diff --git a/src/fixit/format.py b/src/fixit/format.py index 9a332c35..facd8f7b 100644 --- a/src/fixit/format.py +++ b/src/fixit/format.py @@ -48,9 +48,16 @@ def format(self, module: Module, path: Path) -> FileContent: import ufmt.util mode = ufmt.util.make_black_config(path) - content = black.format_file_contents( - module.bytes.decode("utf-8"), fast=False, mode=mode - ) + try: + content = black.format_file_contents( + module.bytes.decode("utf-8"), fast=False, mode=mode + ) + except black.NothingChanged: + # black signals "this is already formatted" by raising rather than + # by returning the content unchanged. That is a successful no-op, + # and letting it propagate discards the autofix that produced this + # module. + return module.bytes return content.encode("utf-8") diff --git a/src/fixit/tests/__init__.py b/src/fixit/tests/__init__.py index ef740dcb..2d842fe6 100644 --- a/src/fixit/tests/__init__.py +++ b/src/fixit/tests/__init__.py @@ -9,6 +9,7 @@ from fixit.testing import add_lint_rule_tests_to_module from .config import ConfigTest from .engine import EngineTest +from .format import FormatTest from .ftypes import TypesTest from .rule import RuleTest, RunnerTest from .smoke import SmokeTest diff --git a/src/fixit/tests/format.py b/src/fixit/tests/format.py new file mode 100644 index 00000000..d8d03a81 --- /dev/null +++ b/src/fixit/tests/format.py @@ -0,0 +1,34 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +from pathlib import Path +from unittest import TestCase + +import libcst + +from ..format import format_module +from ..ftypes import Config + +FAKE_PATH = Path("fake.py") + + +class FormatTest(TestCase): + def test_black_already_formatted(self) -> None: + # black reports "already formatted" by raising rather than by returning + # the content unchanged. Letting that escape loses the autofix that + # produced the module, so the formatter has to treat it as a no-op. + config = Config(path=FAKE_PATH, formatter="black") + for content, expected in ( + (b"x = 1\n", b"x = 1\n"), + (b"x=1\n", b"x = 1\n"), + ): + with self.subTest(content=content): + module = libcst.parse_module(content) + self.assertEqual(expected, format_module(module, FAKE_PATH, config)) + + def test_no_formatter_returns_module_bytes(self) -> None: + config = Config(path=FAKE_PATH) + module = libcst.parse_module(b"x=1\n") + self.assertEqual(b"x=1\n", format_module(module, FAKE_PATH, config))