Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a crash when # fmt: on is used on a different block level than # fmt: off #3281

Merged
merged 5 commits into from Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Expand Up @@ -10,6 +10,9 @@

<!-- Changes that affect Black's stable style -->

- Fix a crash when `# fmt: on` is used on a different block level as `# fmt: off`
yilei marked this conversation as resolved.
Show resolved Hide resolved
(#3281)

### Preview style

<!-- Changes that affect Black's preview style -->
Expand Down
4 changes: 2 additions & 2 deletions docs/contributing/reference/reference_functions.rst
Expand Up @@ -137,9 +137,9 @@ Utilities

.. autofunction:: black.comments.is_fmt_on

.. autofunction:: black.comments.contains_fmt_on_at_column
.. autofunction:: black.comments.children_contains_fmt_on

.. autofunction:: black.nodes.first_leaf_column
.. autofunction:: black.nodes.first_leaf_of

.. autofunction:: black.linegen.generate_trailers_to_omit

Expand Down
34 changes: 16 additions & 18 deletions src/black/comments.py
Expand Up @@ -14,11 +14,12 @@
STANDALONE_COMMENT,
WHITESPACE,
container_of,
first_leaf_column,
first_leaf_of,
preceding_leaf,
syms,
)
from blib2to3.pgen2 import token
from blib2to3.pytree import Leaf, Node, type_repr
from blib2to3.pytree import Leaf, Node

# types
LN = Union[Leaf, Node]
Expand Down Expand Up @@ -230,7 +231,7 @@ def generate_ignored_nodes(
return

# fix for fmt: on in children
if contains_fmt_on_at_column(container, leaf.column, preview=preview):
if children_contains_fmt_on(container, preview=preview):
for child in container.children:
if isinstance(child, Leaf) and is_fmt_on(child, preview=preview):
if child.type in CLOSING_BRACKETS:
Expand All @@ -240,10 +241,14 @@ def generate_ignored_nodes(
# The alternative is to fail the formatting.
yield child
return
if contains_fmt_on_at_column(child, leaf.column, preview=preview):
if children_contains_fmt_on(child, preview=preview):
return
yield child
else:
if container.type == token.DEDENT and container.next_sibling is None:
# This can happen when there is no matching `# fmt: on` comment at the
# same level as `# fmt: on`. We need to keep this DEDENT.
return
yield container
container = container.next_sibling

Expand All @@ -268,17 +273,15 @@ def _generate_ignored_nodes_from_fmt_skip(
for sibling in siblings:
yield sibling
elif (
parent is not None
and type_repr(parent.type) == "suite"
and leaf.type == token.NEWLINE
parent is not None and parent.type == syms.suite and leaf.type == token.NEWLINE
):
# The `# fmt: skip` is on the colon line of the if/while/def/class/...
# statements. The ignored nodes should be previous siblings of the
# parent suite node.
leaf.prefix = ""
ignored_nodes: List[LN] = []
parent_sibling = parent.prev_sibling
while parent_sibling is not None and type_repr(parent_sibling.type) != "suite":
while parent_sibling is not None and parent_sibling.type != syms.suite:
ignored_nodes.insert(0, parent_sibling)
parent_sibling = parent_sibling.prev_sibling
# Special case for `async_stmt` where the ASYNC token is on the
Expand Down Expand Up @@ -306,17 +309,12 @@ def is_fmt_on(container: LN, preview: bool) -> bool:
return fmt_on


def contains_fmt_on_at_column(container: LN, column: int, *, preview: bool) -> bool:
"""Determine if children at a given column have formatting switched on."""
def children_contains_fmt_on(container: LN, *, preview: bool) -> bool:
"""Determine if children have formatting switched on."""
for child in container.children:
if (
isinstance(child, Node)
and first_leaf_column(child) == column
or isinstance(child, Leaf)
and child.column == column
):
if is_fmt_on(child, preview=preview):
return True
leaf = first_leaf_of(child)
if leaf is not None and is_fmt_on(leaf, preview=preview):
return True

return False

Expand Down
14 changes: 8 additions & 6 deletions src/black/nodes.py
Expand Up @@ -502,12 +502,14 @@ def container_of(leaf: Leaf) -> LN:
return container


def first_leaf_column(node: Node) -> Optional[int]:
"""Returns the column of the first leaf child of a node."""
for child in node.children:
if isinstance(child, Leaf):
return child.column
return None
def first_leaf_of(node: LN) -> Optional[Leaf]:
"""Returns the first leaf of the node tree."""
if isinstance(node, Leaf):
return node
if node.children:
return first_leaf_of(node.children[0])
else:
return None


def is_arith_like(node: LN) -> bool:
Expand Down
122 changes: 122 additions & 0 deletions tests/data/simple_cases/fmtonoff5.py
Expand Up @@ -34,3 +34,125 @@ def test_func():
return True

return False


# Regression test for https://github.com/psf/black/issues/2567.
if True:
# fmt: off
for _ in range(1):
yilei marked this conversation as resolved.
Show resolved Hide resolved
# fmt: on
print ( "This won't be formatted" )
print ( "This won't be formatted either" )
else:
print ( "This will be formatted" )


# Regression test for https://github.com/psf/black/issues/3184.
class A:
async def call(param):
if param:
# fmt: off
if param[0:4] in (
"ABCD", "EFGH"
) :
# fmt: on
print ( "This won't be formatted" )

elif param[0:4] in ("ZZZZ",):
print ( "This won't be formatted either" )

print ( "This will be formatted" )


# Regression test for https://github.com/psf/black/issues/2985
class Named(t.Protocol):
# fmt: off
@property
def this_wont_be_formatted ( self ) -> str: ...

class Factory(t.Protocol):
def this_will_be_formatted ( self, **kwargs ) -> Named: ...
# fmt: on


# output


# Regression test for https://github.com/psf/black/issues/3129.
setup(
entry_points={
# fmt: off
"console_scripts": [
"foo-bar"
"=foo.bar.:main",
# fmt: on
] # Includes an formatted indentation.
},
)


# Regression test for https://github.com/psf/black/issues/2015.
run(
# fmt: off
[
"ls",
"-la",
]
# fmt: on
+ path,
check=True,
)


# Regression test for https://github.com/psf/black/issues/3026.
def test_func():
# yapf: disable
if unformatted( args ):
return True
# yapf: enable
elif b:
return True

return False


# Regression test for https://github.com/psf/black/issues/2567.
if True:
# fmt: off
for _ in range(1):
# fmt: on
print ( "This won't be formatted" )
print ( "This won't be formatted either" )
else:
print("This will be formatted")


# Regression test for https://github.com/psf/black/issues/3184.
class A:
async def call(param):
if param:
# fmt: off
if param[0:4] in (
"ABCD", "EFGH"
) :
# fmt: on
print ( "This won't be formatted" )

elif param[0:4] in ("ZZZZ",):
print ( "This won't be formatted either" )

print("This will be formatted")


# Regression test for https://github.com/psf/black/issues/2985
class Named(t.Protocol):
# fmt: off
@property
def this_wont_be_formatted ( self ) -> str: ...


class Factory(t.Protocol):
def this_will_be_formatted(self, **kwargs) -> Named:
...

# fmt: on