Skip to content

Commit 0f376e0

Browse files
Fix crashes when fmt directives are indented (#4856)
* Use `_contains_fmt_directive` everywhere Signed-off-by: cobalt <[email protected]> * Remove underscore prefix Signed-off-by: cobalt <[email protected]> * Add tests Signed-off-by: cobalt <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update changelog Signed-off-by: cobalt <[email protected]> * Remove duplicate tests * fix `visit_STANDALONE_COMMENT`'s `is_fmt_off_block` check Signed-off-by: cobalt <[email protected]> * Prevent wrapping when comments are in arrays Signed-off-by: cobalt <[email protected]> --------- Signed-off-by: cobalt <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a8bfcc1 commit 0f376e0

File tree

4 files changed

+110
-28
lines changed

4 files changed

+110
-28
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
- Fix bug where comments preceding `# fmt: off`/`# fmt: on` blocks were incorrectly
1919
removed, particularly affecting Jupytext's `# %% [markdown]` comments (#4845)
20+
- Fix possible crash when `fmt: ` directives aren't on the top level (#4856)
2021

2122
### Preview style
2223

src/black/comments.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def make_comment(content: str, mode: Mode) -> str:
164164
return "#"
165165

166166
# Preserve comments with fmt directives exactly as-is
167-
if content.startswith("#") and _contains_fmt_directive(content):
167+
if content.startswith("#") and contains_fmt_directive(content):
168168
return content
169169

170170
if content[0] == "#":
@@ -205,8 +205,8 @@ def _should_process_fmt_comment(
205205
206206
Returns (should_process, is_fmt_off, is_fmt_skip).
207207
"""
208-
is_fmt_off = _contains_fmt_directive(comment.value, FMT_OFF)
209-
is_fmt_skip = _contains_fmt_directive(comment.value, FMT_SKIP)
208+
is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF)
209+
is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP)
210210

211211
if not is_fmt_off and not is_fmt_skip:
212212
return False, False, False
@@ -258,9 +258,13 @@ def _handle_comment_only_fmt_block(
258258
fmt_off_idx = None
259259
fmt_on_idx = None
260260
for idx, c in enumerate(all_comments):
261-
if fmt_off_idx is None and c.value in FMT_OFF:
261+
if fmt_off_idx is None and contains_fmt_directive(c.value, FMT_OFF):
262262
fmt_off_idx = idx
263-
if fmt_off_idx is not None and idx > fmt_off_idx and c.value in FMT_ON:
263+
if (
264+
fmt_off_idx is not None
265+
and idx > fmt_off_idx
266+
and contains_fmt_directive(c.value, FMT_ON)
267+
):
264268
fmt_on_idx = idx
265269
break
266270

@@ -401,7 +405,7 @@ def _handle_regular_fmt_block(
401405
parent = first.parent
402406
prefix = first.prefix
403407

404-
if comment.value in FMT_OFF:
408+
if contains_fmt_directive(comment.value, FMT_OFF):
405409
first.prefix = prefix[comment.consumed :]
406410
if is_fmt_skip:
407411
first.prefix = ""
@@ -412,7 +416,7 @@ def _handle_regular_fmt_block(
412416
hidden_value = "".join(str(n) for n in ignored_nodes)
413417
comment_lineno = leaf.lineno - comment.newlines
414418

415-
if comment.value in FMT_OFF:
419+
if contains_fmt_directive(comment.value, FMT_OFF):
416420
fmt_off_prefix = ""
417421
if len(lines) > 0 and not any(
418422
line[0] <= comment_lineno <= line[1] for line in lines
@@ -460,7 +464,7 @@ def generate_ignored_nodes(
460464
If comment is skip, returns leaf only.
461465
Stops at the end of the block.
462466
"""
463-
if _contains_fmt_directive(comment.value, FMT_SKIP):
467+
if contains_fmt_directive(comment.value, FMT_SKIP):
464468
yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
465469
return
466470
container: LN | None = container_of(leaf)
@@ -717,9 +721,9 @@ def is_fmt_on(container: LN, mode: Mode) -> bool:
717721
"""
718722
fmt_on = False
719723
for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
720-
if comment.value in FMT_ON:
724+
if contains_fmt_directive(comment.value, FMT_ON):
721725
fmt_on = True
722-
elif comment.value in FMT_OFF:
726+
elif contains_fmt_directive(comment.value, FMT_OFF):
723727
fmt_on = False
724728
return fmt_on
725729

@@ -748,7 +752,7 @@ def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
748752
return False
749753

750754

751-
def _contains_fmt_directive(
755+
def contains_fmt_directive(
752756
comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
753757
) -> bool:
754758
"""

src/black/linegen.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from black.comments import (
2121
FMT_OFF,
2222
FMT_ON,
23-
_contains_fmt_directive,
23+
contains_fmt_directive,
2424
generate_comments,
2525
list_comments,
2626
)
@@ -387,7 +387,8 @@ def visit_ENDMARKER(self, leaf: Leaf) -> Iterator[Line]:
387387
yield from self.line()
388388

389389
def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
390-
if not self.current_line.bracket_tracker.any_open_brackets():
390+
any_open_brackets = self.current_line.bracket_tracker.any_open_brackets()
391+
if not any_open_brackets:
391392
yield from self.line()
392393
# STANDALONE_COMMENT nodes created by our special handling in
393394
# normalize_fmt_off for comment-only blocks have fmt:off as the first
@@ -398,18 +399,11 @@ def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
398399
# visit_default.
399400
value = leaf.value
400401
lines = value.splitlines()
401-
if len(lines) >= 2:
402-
# Check if first line (after stripping whitespace) is exactly a
403-
# fmt:off directive
404-
first_line = lines[0].lstrip()
405-
first_is_fmt_off = first_line in FMT_OFF
406-
# Check if last line (after stripping whitespace) is exactly a
407-
# fmt:on directive
408-
last_line = lines[-1].lstrip()
409-
last_is_fmt_on = last_line in FMT_ON
410-
is_fmt_off_block = first_is_fmt_off and last_is_fmt_on
411-
else:
412-
is_fmt_off_block = False
402+
is_fmt_off_block = (
403+
len(lines) >= 2
404+
and contains_fmt_directive(lines[0], FMT_OFF)
405+
and contains_fmt_directive(lines[-1], FMT_ON)
406+
)
413407
if is_fmt_off_block:
414408
# This is a fmt:off/on block from normalize_fmt_off - we still need
415409
# to process any prefix comments (like markdown comments) but append
@@ -418,7 +412,7 @@ def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
418412
# Only process prefix comments if there actually is a prefix with comments
419413
if leaf.prefix and any(
420414
line.strip().startswith("#")
421-
and not _contains_fmt_directive(line.strip())
415+
and not contains_fmt_directive(line.strip())
422416
for line in leaf.prefix.split("\n")
423417
):
424418
for comment in generate_comments(leaf, mode=self.mode):
@@ -429,7 +423,8 @@ def visit_STANDALONE_COMMENT(self, leaf: Leaf) -> Iterator[Line]:
429423
leaf.prefix = ""
430424

431425
self.current_line.append(leaf)
432-
yield from self.line()
426+
if not any_open_brackets:
427+
yield from self.line()
433428
else:
434429
# Normal standalone comment - process through visit_default
435430
yield from self.visit_default(leaf)
@@ -1484,7 +1479,7 @@ def normalize_invisible_parens(
14841479
existing visible parentheses for other tuples and generator expressions.
14851480
"""
14861481
for pc in list_comments(node.prefix, is_endmarker=False, mode=mode):
1487-
if pc.value in FMT_OFF:
1482+
if contains_fmt_directive(pc.value, FMT_OFF):
14881483
# This `node` has a prefix with `# fmt: off`, don't mess with parens.
14891484
return
14901485

tests/data/cases/fmtskip11.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,85 @@ def foo():
44

55
# comment 1 # fmt: skip
66
# comment 2
7+
8+
[
9+
(1, 2),
10+
# # fmt: off
11+
# (3,
12+
# 4),
13+
# # fmt: on
14+
(5, 6),
15+
]
16+
17+
[
18+
(1, 2),
19+
# # fmt: off
20+
# (3,
21+
# 4),
22+
# fmt: on
23+
(5, 6),
24+
]
25+
26+
27+
[
28+
(1, 2),
29+
# fmt: off
30+
# (3,
31+
# 4),
32+
# # fmt: on
33+
(5, 6),
34+
]
35+
36+
37+
[
38+
(1, 2),
39+
# fmt: off
40+
# (3,
41+
# 4),
42+
# fmt: on
43+
(5, 6),
44+
]
45+
46+
[
47+
(1, 2),
48+
# # fmt: off
49+
(3,
50+
4),
51+
# # fmt: on
52+
(5, 6),
53+
]
54+
55+
[
56+
(1, 2),
57+
# # fmt: off
58+
(3,
59+
4),
60+
# fmt: on
61+
(5, 6),
62+
]
63+
64+
65+
[
66+
(1, 2),
67+
# fmt: off
68+
(3,
69+
4),
70+
# # fmt: on
71+
(5, 6),
72+
]
73+
74+
75+
[
76+
(1, 2),
77+
# fmt: off
78+
(3,
79+
4),
80+
# fmt: on
81+
(5, 6),
82+
]
83+
84+
85+
if False:
86+
# fmt: off # some other comment
87+
pass
88+

0 commit comments

Comments
 (0)