From 7b5a657285f38126bf28483478bbd9ea928077ec Mon Sep 17 00:00:00 2001 From: Samson Umezulike Date: Fri, 15 Mar 2024 19:18:47 +0100 Subject: [PATCH] Fix --line-ranges behavior when ranges are at EOF (#4273) Fixes #4264 --- CHANGES.md | 2 + src/black/__init__.py | 11 +++- src/black/ranges.py | 28 ++++++++ tests/data/cases/line_ranges_exceeding_end.py | 36 ++++++++++ .../data/cases/line_ranges_outside_source.py | 7 ++ tests/test_ranges.py | 66 ++++++++++++++++++- 6 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 tests/data/cases/line_ranges_exceeding_end.py create mode 100644 tests/data/cases/line_ranges_outside_source.py diff --git a/CHANGES.md b/CHANGES.md index e0a034b759..c255c2a834 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,8 @@ of Black would incorrectly format the contents of certain unusual f-strings containing nested strings with the same quote type. Now, Black will crash on such strings until support for the new f-string syntax is implemented. (#4270) +- Fixed a bug where line-ranges exceeding the last code line would not work as expected + (#4273) ### Preview style diff --git a/src/black/__init__.py b/src/black/__init__.py index da884e6027..6f0e128f56 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -84,7 +84,12 @@ parse_ast, stringify_ast, ) -from black.ranges import adjusted_lines, convert_unchanged_lines, parse_line_ranges +from black.ranges import ( + adjusted_lines, + convert_unchanged_lines, + parse_line_ranges, + sanitized_lines, +) from black.report import Changed, NothingChanged, Report from black.trans import iter_fexpr_spans from blib2to3.pgen2 import token @@ -1220,6 +1225,10 @@ def f( hey """ + if lines: + lines = sanitized_lines(lines, src_contents) + if not lines: + return src_contents # Nothing to format dst_contents = _format_str_once(src_contents, mode=mode, lines=lines) # Forced second pass to work around optional trailing commas (becoming # forced trailing commas on pass 2) interacting differently with optional diff --git a/src/black/ranges.py b/src/black/ranges.py index 06fa879055..1ecaf7b0ae 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -45,6 +45,34 @@ def is_valid_line_range(lines: Tuple[int, int]) -> bool: return not lines or lines[0] <= lines[1] +def sanitized_lines( + lines: Collection[Tuple[int, int]], src_contents: str +) -> Collection[Tuple[int, int]]: + """Returns the valid line ranges for the given source. + + This removes ranges that are entirely outside the valid lines. + + Other ranges are normalized so that the start values are at least 1 and the + end values are at most the (1-based) index of the last source line. + """ + if not src_contents: + return [] + good_lines = [] + src_line_count = src_contents.count("\n") + if not src_contents.endswith("\n"): + src_line_count += 1 + for start, end in lines: + if start > src_line_count: + continue + # line-ranges are 1-based + start = max(start, 1) + if end < start: + continue + end = min(end, src_line_count) + good_lines.append((start, end)) + return good_lines + + def adjusted_lines( lines: Collection[Tuple[int, int]], original_source: str, diff --git a/tests/data/cases/line_ranges_exceeding_end.py b/tests/data/cases/line_ranges_exceeding_end.py new file mode 100644 index 0000000000..8f17491f68 --- /dev/null +++ b/tests/data/cases/line_ranges_exceeding_end.py @@ -0,0 +1,36 @@ +# flags: --line-ranges=6-1000 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass + +# output +# flags: --line-ranges=6-1000 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines. +def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo3( + parameter_1, + parameter_2, + parameter_3, + parameter_4, + parameter_5, + parameter_6, + parameter_7, +): + pass + + +def foo4( + parameter_1, + parameter_2, + parameter_3, + parameter_4, + parameter_5, + parameter_6, + parameter_7, +): + pass diff --git a/tests/data/cases/line_ranges_outside_source.py b/tests/data/cases/line_ranges_outside_source.py new file mode 100644 index 0000000000..edec9015ff --- /dev/null +++ b/tests/data/cases/line_ranges_outside_source.py @@ -0,0 +1,7 @@ +# flags: --line-ranges=5000-6000 +# NOTE: If you need to modify this file, pay special attention to the --line-ranges= +# flag above as it's formatting specifically these lines, in this case none. +def foo1(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo2(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo3(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass +def foo4(parameter_1, parameter_2, parameter_3, parameter_4, parameter_5, parameter_6, parameter_7): pass diff --git a/tests/test_ranges.py b/tests/test_ranges.py index d9fa9171a7..a3028babf5 100644 --- a/tests/test_ranges.py +++ b/tests/test_ranges.py @@ -4,7 +4,7 @@ import pytest -from black.ranges import adjusted_lines +from black.ranges import adjusted_lines, sanitized_lines @pytest.mark.parametrize( @@ -183,3 +183,67 @@ def test_diffs(lines: List[Tuple[int, int]], adjusted: List[Tuple[int, int]]) -> 12. # last line changed """ assert adjusted == adjusted_lines(lines, original_source, modified_source) + + +@pytest.mark.parametrize( + "lines,sanitized", + [ + ( + [(1, 4)], + [(1, 4)], + ), + ( + [(2, 3)], + [(2, 3)], + ), + ( + [(2, 10)], + [(2, 4)], + ), + ( + [(0, 3)], + [(1, 3)], + ), + ( + [(0, 10)], + [(1, 4)], + ), + ( + [(-2, 3)], + [(1, 3)], + ), + ( + [(0, 0)], + [], + ), + ( + [(-2, -1)], + [], + ), + ( + [(-1, 0)], + [], + ), + ( + [(3, 1), (1, 3), (5, 6)], + [(1, 3)], + ), + ], +) +def test_sanitize( + lines: List[Tuple[int, int]], sanitized: List[Tuple[int, int]] +) -> None: + source = """\ +1. import re +2. def func(arg1, +3. arg2, arg3): +4. pass +""" + assert sanitized == sanitized_lines(lines, source) + + source_no_trailing_nl = """\ + 1. import re + 2. def func(arg1, + 3. arg2, arg3): + 4. pass""" + assert sanitized == sanitized_lines(lines, source_no_trailing_nl)