Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
389d72a136
commit
aa4c067ea8
1685 changed files with 393439 additions and 71932 deletions
827
.venv_codegen/Lib/site-packages/black/comments.py
Normal file
827
.venv_codegen/Lib/site-packages/black/comments.py
Normal file
|
|
@ -0,0 +1,827 @@
|
|||
import re
|
||||
from collections.abc import Collection, Iterator
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Final, Union
|
||||
|
||||
from black.mode import Mode
|
||||
from black.nodes import (
|
||||
CLOSING_BRACKETS,
|
||||
STANDALONE_COMMENT,
|
||||
STATEMENT,
|
||||
WHITESPACE,
|
||||
container_of,
|
||||
first_leaf_of,
|
||||
is_type_comment_string,
|
||||
make_simple_prefix,
|
||||
preceding_leaf,
|
||||
syms,
|
||||
)
|
||||
from blib2to3.pgen2 import token
|
||||
from blib2to3.pytree import Leaf, Node
|
||||
|
||||
# types
|
||||
LN = Union[Leaf, Node]
|
||||
|
||||
FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"}
|
||||
FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"}
|
||||
FMT_ON: Final = {"# fmt: on", "# fmt:on", "# yapf: enable"}
|
||||
|
||||
# Compound statements we care about for fmt: skip handling
|
||||
# (excludes except_clause and case_block which aren't standalone compound statements)
|
||||
_COMPOUND_STATEMENTS: Final = STATEMENT - {syms.except_clause, syms.case_block}
|
||||
|
||||
COMMENT_EXCEPTIONS = " !:#'"
|
||||
_COMMENT_PREFIX = "# "
|
||||
_COMMENT_LIST_SEPARATOR = ";"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProtoComment:
|
||||
"""Describes a piece of syntax that is a comment.
|
||||
|
||||
It's not a :class:`blib2to3.pytree.Leaf` so that:
|
||||
|
||||
* it can be cached (`Leaf` objects should not be reused more than once as
|
||||
they store their lineno, column, prefix, and parent information);
|
||||
* `newlines` and `consumed` fields are kept separate from the `value`. This
|
||||
simplifies handling of special marker comments like ``# fmt: off/on``.
|
||||
"""
|
||||
|
||||
type: int # token.COMMENT or STANDALONE_COMMENT
|
||||
value: str # content of the comment
|
||||
newlines: int # how many newlines before the comment
|
||||
consumed: int # how many characters of the original leaf's prefix did we consume
|
||||
form_feed: bool # is there a form feed before the comment
|
||||
leading_whitespace: str # leading whitespace before the comment, if any
|
||||
|
||||
|
||||
def generate_comments(leaf: LN, mode: Mode) -> Iterator[Leaf]:
|
||||
"""Clean the prefix of the `leaf` and generate comments from it, if any.
|
||||
|
||||
Comments in lib2to3 are shoved into the whitespace prefix. This happens
|
||||
in `pgen2/driver.py:Driver.parse_tokens()`. This was a brilliant implementation
|
||||
move because it does away with modifying the grammar to include all the
|
||||
possible places in which comments can be placed.
|
||||
|
||||
The sad consequence for us though is that comments don't "belong" anywhere.
|
||||
This is why this function generates simple parentless Leaf objects for
|
||||
comments. We simply don't know what the correct parent should be.
|
||||
|
||||
No matter though, we can live without this. We really only need to
|
||||
differentiate between inline and standalone comments. The latter don't
|
||||
share the line with any code.
|
||||
|
||||
Inline comments are emitted as regular token.COMMENT leaves. Standalone
|
||||
are emitted with a fake STANDALONE_COMMENT token identifier.
|
||||
"""
|
||||
total_consumed = 0
|
||||
for pc in list_comments(
|
||||
leaf.prefix, is_endmarker=leaf.type == token.ENDMARKER, mode=mode
|
||||
):
|
||||
total_consumed = pc.consumed
|
||||
prefix = make_simple_prefix(pc.newlines, pc.form_feed)
|
||||
yield Leaf(pc.type, pc.value, prefix=prefix)
|
||||
normalize_trailing_prefix(leaf, total_consumed)
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def list_comments(prefix: str, *, is_endmarker: bool, mode: Mode) -> list[ProtoComment]:
|
||||
"""Return a list of :class:`ProtoComment` objects parsed from the given `prefix`."""
|
||||
result: list[ProtoComment] = []
|
||||
if not prefix or "#" not in prefix:
|
||||
return result
|
||||
|
||||
consumed = 0
|
||||
nlines = 0
|
||||
ignored_lines = 0
|
||||
form_feed = False
|
||||
for index, full_line in enumerate(re.split("\r?\n|\r", prefix)):
|
||||
consumed += len(full_line) + 1 # adding the length of the split '\n'
|
||||
match = re.match(r"^(\s*)(\S.*|)$", full_line)
|
||||
assert match
|
||||
whitespace, line = match.groups()
|
||||
if not line:
|
||||
nlines += 1
|
||||
if "\f" in full_line:
|
||||
form_feed = True
|
||||
if not line.startswith("#"):
|
||||
# Escaped newlines outside of a comment are not really newlines at
|
||||
# all. We treat a single-line comment following an escaped newline
|
||||
# as a simple trailing comment.
|
||||
if line.endswith("\\"):
|
||||
ignored_lines += 1
|
||||
continue
|
||||
|
||||
if index == ignored_lines and not is_endmarker:
|
||||
comment_type = token.COMMENT # simple trailing comment
|
||||
else:
|
||||
comment_type = STANDALONE_COMMENT
|
||||
comment = make_comment(line, mode=mode)
|
||||
result.append(
|
||||
ProtoComment(
|
||||
type=comment_type,
|
||||
value=comment,
|
||||
newlines=nlines,
|
||||
consumed=consumed,
|
||||
form_feed=form_feed,
|
||||
leading_whitespace=whitespace,
|
||||
)
|
||||
)
|
||||
form_feed = False
|
||||
nlines = 0
|
||||
return result
|
||||
|
||||
|
||||
def normalize_trailing_prefix(leaf: LN, total_consumed: int) -> None:
|
||||
"""Normalize the prefix that's left over after generating comments.
|
||||
|
||||
Note: don't use backslashes for formatting or you'll lose your voting rights.
|
||||
"""
|
||||
remainder = leaf.prefix[total_consumed:]
|
||||
if "\\" not in remainder:
|
||||
nl_count = remainder.count("\n")
|
||||
form_feed = "\f" in remainder and remainder.endswith("\n")
|
||||
leaf.prefix = make_simple_prefix(nl_count, form_feed)
|
||||
return
|
||||
|
||||
leaf.prefix = ""
|
||||
|
||||
|
||||
def make_comment(content: str, mode: Mode) -> str:
|
||||
"""Return a consistently formatted comment from the given `content` string.
|
||||
|
||||
All comments (except for "##", "#!", "#:", '#'") should have a single
|
||||
space between the hash sign and the content.
|
||||
|
||||
If `content` didn't start with a hash sign, one is provided.
|
||||
|
||||
Comments containing fmt directives are preserved exactly as-is to respect
|
||||
user intent (e.g., `#no space # fmt: skip` stays as-is).
|
||||
"""
|
||||
content = content.rstrip()
|
||||
if not content:
|
||||
return "#"
|
||||
|
||||
# Preserve comments with fmt directives exactly as-is
|
||||
if content.startswith("#") and contains_fmt_directive(content):
|
||||
return content
|
||||
|
||||
if content[0] == "#":
|
||||
content = content[1:]
|
||||
if (
|
||||
content
|
||||
and content[0] == "\N{NO-BREAK SPACE}"
|
||||
and not is_type_comment_string("# " + content.lstrip(), mode=mode)
|
||||
):
|
||||
content = " " + content[1:] # Replace NBSP by a simple space
|
||||
if (
|
||||
content
|
||||
and "\N{NO-BREAK SPACE}" not in content
|
||||
and is_type_comment_string("#" + content, mode=mode)
|
||||
):
|
||||
type_part, value_part = content.split(":", 1)
|
||||
content = type_part.strip() + ": " + value_part.strip()
|
||||
|
||||
if content and content[0] not in COMMENT_EXCEPTIONS:
|
||||
content = " " + content
|
||||
return "#" + content
|
||||
|
||||
|
||||
def normalize_fmt_off(
|
||||
node: Node, mode: Mode, lines: Collection[tuple[int, int]]
|
||||
) -> None:
|
||||
"""Convert content between `# fmt: off`/`# fmt: on` into standalone comments."""
|
||||
try_again = True
|
||||
while try_again:
|
||||
try_again = convert_one_fmt_off_pair(node, mode, lines)
|
||||
|
||||
|
||||
def _should_process_fmt_comment(
|
||||
comment: ProtoComment, leaf: Leaf
|
||||
) -> tuple[bool, bool, bool]:
|
||||
"""Check if comment should be processed for fmt handling.
|
||||
|
||||
Returns (should_process, is_fmt_off, is_fmt_skip).
|
||||
"""
|
||||
is_fmt_off = contains_fmt_directive(comment.value, FMT_OFF)
|
||||
is_fmt_skip = contains_fmt_directive(comment.value, FMT_SKIP)
|
||||
|
||||
if not is_fmt_off and not is_fmt_skip:
|
||||
return False, False, False
|
||||
|
||||
# Invalid use when `# fmt: off` is applied before a closing bracket
|
||||
if is_fmt_off and leaf.type in CLOSING_BRACKETS:
|
||||
return False, False, False
|
||||
|
||||
return True, is_fmt_off, is_fmt_skip
|
||||
|
||||
|
||||
def _is_valid_standalone_fmt_comment(
|
||||
comment: ProtoComment, leaf: Leaf, is_fmt_off: bool, is_fmt_skip: bool
|
||||
) -> bool:
|
||||
"""Check if comment is a valid standalone fmt directive.
|
||||
|
||||
We only want standalone comments. If there's no previous leaf or if
|
||||
the previous leaf is indentation, it's a standalone comment in disguise.
|
||||
"""
|
||||
if comment.type == STANDALONE_COMMENT:
|
||||
return True
|
||||
|
||||
prev = preceding_leaf(leaf)
|
||||
if not prev:
|
||||
return True
|
||||
|
||||
# Treat STANDALONE_COMMENT nodes as whitespace for check
|
||||
if is_fmt_off and prev.type not in WHITESPACE and prev.type != STANDALONE_COMMENT:
|
||||
return False
|
||||
if is_fmt_skip and prev.type in WHITESPACE:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_comment_only_fmt_block(
|
||||
leaf: Leaf,
|
||||
comment: ProtoComment,
|
||||
previous_consumed: int,
|
||||
mode: Mode,
|
||||
) -> bool:
|
||||
"""Handle fmt:off/on blocks that contain only comments.
|
||||
|
||||
Returns True if a block was converted, False otherwise.
|
||||
"""
|
||||
all_comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
|
||||
|
||||
# Find the first fmt:off and its matching fmt:on
|
||||
fmt_off_idx = None
|
||||
fmt_on_idx = None
|
||||
for idx, c in enumerate(all_comments):
|
||||
if fmt_off_idx is None and contains_fmt_directive(c.value, FMT_OFF):
|
||||
fmt_off_idx = idx
|
||||
if (
|
||||
fmt_off_idx is not None
|
||||
and idx > fmt_off_idx
|
||||
and contains_fmt_directive(c.value, FMT_ON)
|
||||
):
|
||||
fmt_on_idx = idx
|
||||
break
|
||||
|
||||
# Only proceed if we found both directives
|
||||
if fmt_on_idx is None or fmt_off_idx is None:
|
||||
return False
|
||||
|
||||
comment = all_comments[fmt_off_idx]
|
||||
fmt_on_comment = all_comments[fmt_on_idx]
|
||||
original_prefix = leaf.prefix
|
||||
|
||||
# Build the hidden value
|
||||
start_pos = comment.consumed
|
||||
end_pos = fmt_on_comment.consumed
|
||||
content_between_and_fmt_on = original_prefix[start_pos:end_pos]
|
||||
hidden_value = comment.value + "\n" + content_between_and_fmt_on
|
||||
|
||||
if hidden_value.endswith("\n"):
|
||||
hidden_value = hidden_value[:-1]
|
||||
|
||||
# Build the standalone comment prefix - preserve all content before fmt:off
|
||||
# including any comments that precede it
|
||||
if fmt_off_idx == 0:
|
||||
# No comments before fmt:off, use previous_consumed
|
||||
pre_fmt_off_consumed = previous_consumed
|
||||
else:
|
||||
# Use the consumed position of the last comment before fmt:off
|
||||
# This preserves all comments and content before the fmt:off directive
|
||||
pre_fmt_off_consumed = all_comments[fmt_off_idx - 1].consumed
|
||||
|
||||
standalone_comment_prefix = (
|
||||
original_prefix[:pre_fmt_off_consumed] + "\n" * comment.newlines
|
||||
)
|
||||
|
||||
fmt_off_prefix = original_prefix.split(comment.value)[0]
|
||||
if "\n" in fmt_off_prefix:
|
||||
fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
|
||||
standalone_comment_prefix += fmt_off_prefix
|
||||
|
||||
# Update leaf prefix
|
||||
leaf.prefix = original_prefix[fmt_on_comment.consumed :]
|
||||
|
||||
# Insert the STANDALONE_COMMENT
|
||||
parent = leaf.parent
|
||||
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (prefix only)"
|
||||
|
||||
leaf_idx = None
|
||||
for idx, child in enumerate(parent.children):
|
||||
if child is leaf:
|
||||
leaf_idx = idx
|
||||
break
|
||||
|
||||
assert leaf_idx is not None, "INTERNAL ERROR: fmt: on/off handling (leaf index)"
|
||||
|
||||
parent.insert_child(
|
||||
leaf_idx,
|
||||
Leaf(
|
||||
STANDALONE_COMMENT,
|
||||
hidden_value,
|
||||
prefix=standalone_comment_prefix,
|
||||
fmt_pass_converted_first_leaf=None,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def convert_one_fmt_off_pair(
|
||||
node: Node, mode: Mode, lines: Collection[tuple[int, int]]
|
||||
) -> bool:
|
||||
"""Convert content of a single `# fmt: off`/`# fmt: on` into a standalone comment.
|
||||
|
||||
Returns True if a pair was converted.
|
||||
"""
|
||||
for leaf in node.leaves():
|
||||
# Skip STANDALONE_COMMENT nodes that were created by fmt:off/on/skip processing
|
||||
# to avoid reprocessing them in subsequent iterations
|
||||
if leaf.type == STANDALONE_COMMENT and hasattr(
|
||||
leaf, "fmt_pass_converted_first_leaf"
|
||||
):
|
||||
continue
|
||||
|
||||
previous_consumed = 0
|
||||
for comment in list_comments(leaf.prefix, is_endmarker=False, mode=mode):
|
||||
should_process, is_fmt_off, is_fmt_skip = _should_process_fmt_comment(
|
||||
comment, leaf
|
||||
)
|
||||
if not should_process:
|
||||
previous_consumed = comment.consumed
|
||||
continue
|
||||
|
||||
if not _is_valid_standalone_fmt_comment(
|
||||
comment, leaf, is_fmt_off, is_fmt_skip
|
||||
):
|
||||
previous_consumed = comment.consumed
|
||||
continue
|
||||
|
||||
ignored_nodes = list(generate_ignored_nodes(leaf, comment, mode))
|
||||
|
||||
# Handle comment-only blocks
|
||||
if not ignored_nodes and is_fmt_off:
|
||||
if _handle_comment_only_fmt_block(
|
||||
leaf, comment, previous_consumed, mode
|
||||
):
|
||||
return True
|
||||
continue
|
||||
|
||||
# Need actual nodes to process
|
||||
if not ignored_nodes:
|
||||
continue
|
||||
|
||||
# Handle regular fmt blocks
|
||||
|
||||
_handle_regular_fmt_block(
|
||||
ignored_nodes,
|
||||
comment,
|
||||
previous_consumed,
|
||||
is_fmt_skip,
|
||||
lines,
|
||||
leaf,
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _handle_regular_fmt_block(
|
||||
ignored_nodes: list[LN],
|
||||
comment: ProtoComment,
|
||||
previous_consumed: int,
|
||||
is_fmt_skip: bool,
|
||||
lines: Collection[tuple[int, int]],
|
||||
leaf: Leaf,
|
||||
) -> None:
|
||||
"""Handle fmt blocks with actual AST nodes."""
|
||||
first = ignored_nodes[0] # Can be a container node with the `leaf`.
|
||||
parent = first.parent
|
||||
prefix = first.prefix
|
||||
|
||||
if contains_fmt_directive(comment.value, FMT_OFF):
|
||||
first.prefix = prefix[comment.consumed :]
|
||||
if is_fmt_skip:
|
||||
first.prefix = ""
|
||||
standalone_comment_prefix = prefix
|
||||
else:
|
||||
standalone_comment_prefix = prefix[:previous_consumed] + "\n" * comment.newlines
|
||||
|
||||
# Ensure STANDALONE_COMMENT nodes have trailing newlines when stringified
|
||||
# This prevents multiple fmt: skip comments from being concatenated on one line
|
||||
parts = []
|
||||
for node in ignored_nodes:
|
||||
if isinstance(node, Leaf) and node.type == STANDALONE_COMMENT:
|
||||
# Add newline after STANDALONE_COMMENT Leaf
|
||||
node_str = str(node)
|
||||
if not node_str.endswith("\n"):
|
||||
node_str += "\n"
|
||||
parts.append(node_str)
|
||||
elif isinstance(node, Node):
|
||||
# For nodes that might contain STANDALONE_COMMENT leaves,
|
||||
# we need custom stringify
|
||||
has_standalone = any(
|
||||
leaf.type == STANDALONE_COMMENT for leaf in node.leaves()
|
||||
)
|
||||
if has_standalone:
|
||||
# Stringify node with STANDALONE_COMMENT leaves having trailing newlines
|
||||
def stringify_node(n: LN) -> str:
|
||||
if isinstance(n, Leaf):
|
||||
if n.type == STANDALONE_COMMENT:
|
||||
result = n.prefix + n.value
|
||||
if not result.endswith("\n"):
|
||||
result += "\n"
|
||||
return result
|
||||
return str(n)
|
||||
else:
|
||||
# For nested nodes, recursively process children
|
||||
return "".join(stringify_node(child) for child in n.children)
|
||||
|
||||
parts.append(stringify_node(node))
|
||||
else:
|
||||
parts.append(str(node))
|
||||
else:
|
||||
parts.append(str(node))
|
||||
|
||||
hidden_value = "".join(parts)
|
||||
comment_lineno = leaf.lineno - comment.newlines
|
||||
|
||||
if contains_fmt_directive(comment.value, FMT_OFF):
|
||||
fmt_off_prefix = ""
|
||||
if len(lines) > 0 and not any(
|
||||
line[0] <= comment_lineno <= line[1] for line in lines
|
||||
):
|
||||
# keeping indentation of comment by preserving original whitespaces.
|
||||
fmt_off_prefix = prefix.split(comment.value)[0]
|
||||
if "\n" in fmt_off_prefix:
|
||||
fmt_off_prefix = fmt_off_prefix.split("\n")[-1]
|
||||
standalone_comment_prefix += fmt_off_prefix
|
||||
hidden_value = comment.value + "\n" + hidden_value
|
||||
|
||||
if is_fmt_skip:
|
||||
hidden_value += comment.leading_whitespace + comment.value
|
||||
|
||||
if hidden_value.endswith("\n"):
|
||||
# That happens when one of the `ignored_nodes` ended with a NEWLINE
|
||||
# leaf (possibly followed by a DEDENT).
|
||||
hidden_value = hidden_value[:-1]
|
||||
|
||||
first_idx: int | None = None
|
||||
for ignored in ignored_nodes:
|
||||
index = ignored.remove()
|
||||
if first_idx is None:
|
||||
first_idx = index
|
||||
|
||||
assert parent is not None, "INTERNAL ERROR: fmt: on/off handling (1)"
|
||||
assert first_idx is not None, "INTERNAL ERROR: fmt: on/off handling (2)"
|
||||
|
||||
parent.insert_child(
|
||||
first_idx,
|
||||
Leaf(
|
||||
STANDALONE_COMMENT,
|
||||
hidden_value,
|
||||
prefix=standalone_comment_prefix,
|
||||
fmt_pass_converted_first_leaf=first_leaf_of(first),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def generate_ignored_nodes(
|
||||
leaf: Leaf, comment: ProtoComment, mode: Mode
|
||||
) -> Iterator[LN]:
|
||||
"""Starting from the container of `leaf`, generate all leaves until `# fmt: on`.
|
||||
|
||||
If comment is skip, returns leaf only.
|
||||
Stops at the end of the block.
|
||||
"""
|
||||
if contains_fmt_directive(comment.value, FMT_SKIP):
|
||||
yield from _generate_ignored_nodes_from_fmt_skip(leaf, comment, mode)
|
||||
return
|
||||
container: LN | None = container_of(leaf)
|
||||
while container is not None and container.type != token.ENDMARKER:
|
||||
if is_fmt_on(container, mode=mode):
|
||||
return
|
||||
|
||||
# fix for fmt: on in children
|
||||
if children_contains_fmt_on(container, mode=mode):
|
||||
for index, child in enumerate(container.children):
|
||||
if isinstance(child, Leaf) and is_fmt_on(child, mode=mode):
|
||||
if child.type in CLOSING_BRACKETS:
|
||||
# This means `# fmt: on` is placed at a different bracket level
|
||||
# than `# fmt: off`. This is an invalid use, but as a courtesy,
|
||||
# we include this closing bracket in the ignored nodes.
|
||||
# The alternative is to fail the formatting.
|
||||
yield child
|
||||
return
|
||||
if (
|
||||
child.type == token.INDENT
|
||||
and index < len(container.children) - 1
|
||||
and children_contains_fmt_on(
|
||||
container.children[index + 1], mode=mode
|
||||
)
|
||||
):
|
||||
# This means `# fmt: on` is placed right after an indentation
|
||||
# level, and we shouldn't swallow the previous INDENT token.
|
||||
return
|
||||
if children_contains_fmt_on(child, mode=mode):
|
||||
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
|
||||
|
||||
|
||||
def _find_compound_statement_context(parent: Node) -> Node | None:
|
||||
"""Return the body node of a compound statement if we should respect fmt: skip.
|
||||
|
||||
This handles one-line compound statements like:
|
||||
if condition: body # fmt: skip
|
||||
|
||||
When Black expands such statements, they temporarily look like:
|
||||
if condition:
|
||||
body # fmt: skip
|
||||
|
||||
In both cases, we want to return the body node (either the simple_stmt directly
|
||||
or the suite containing it).
|
||||
"""
|
||||
if parent.type != syms.simple_stmt:
|
||||
return None
|
||||
|
||||
if not isinstance(parent.parent, Node):
|
||||
return None
|
||||
|
||||
# Case 1: Expanded form after Black's initial formatting pass.
|
||||
# The one-liner has been split across multiple lines:
|
||||
# if True:
|
||||
# print("a"); print("b") # fmt: skip
|
||||
# Structure: compound_stmt -> suite -> simple_stmt
|
||||
if (
|
||||
parent.parent.type == syms.suite
|
||||
and isinstance(parent.parent.parent, Node)
|
||||
and parent.parent.parent.type in _COMPOUND_STATEMENTS
|
||||
):
|
||||
return parent.parent
|
||||
|
||||
# Case 2: Original one-line form from the input source.
|
||||
# The statement is still on a single line:
|
||||
# if True: print("a"); print("b") # fmt: skip
|
||||
# Structure: compound_stmt -> simple_stmt
|
||||
if parent.parent.type in _COMPOUND_STATEMENTS:
|
||||
return parent
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _should_keep_compound_statement_inline(
|
||||
body_node: Node, simple_stmt_parent: Node
|
||||
) -> bool:
|
||||
"""Check if a compound statement should be kept on one line.
|
||||
|
||||
Returns True only for compound statements with semicolon-separated bodies,
|
||||
like: if True: print("a"); print("b") # fmt: skip
|
||||
"""
|
||||
# Check if there are semicolons in the body
|
||||
for leaf in body_node.leaves():
|
||||
if leaf.type == token.SEMI:
|
||||
# Verify it's a single-line body (one simple_stmt)
|
||||
if body_node.type == syms.suite:
|
||||
# After formatting: check suite has one simple_stmt child
|
||||
simple_stmts = [
|
||||
child
|
||||
for child in body_node.children
|
||||
if child.type == syms.simple_stmt
|
||||
]
|
||||
return len(simple_stmts) == 1 and simple_stmts[0] is simple_stmt_parent
|
||||
else:
|
||||
# Original form: body_node IS the simple_stmt
|
||||
return body_node is simple_stmt_parent
|
||||
return False
|
||||
|
||||
|
||||
def _get_compound_statement_header(
|
||||
body_node: Node, simple_stmt_parent: Node
|
||||
) -> list[LN]:
|
||||
"""Get header nodes for a compound statement that should be preserved inline."""
|
||||
if not _should_keep_compound_statement_inline(body_node, simple_stmt_parent):
|
||||
return []
|
||||
|
||||
# Get the compound statement (parent of body)
|
||||
compound_stmt = body_node.parent
|
||||
if compound_stmt is None or compound_stmt.type not in _COMPOUND_STATEMENTS:
|
||||
return []
|
||||
|
||||
# Collect all header leaves before the body
|
||||
header_leaves: list[LN] = []
|
||||
for child in compound_stmt.children:
|
||||
if child is body_node:
|
||||
break
|
||||
if isinstance(child, Leaf):
|
||||
if child.type not in (token.NEWLINE, token.INDENT):
|
||||
header_leaves.append(child)
|
||||
else:
|
||||
header_leaves.extend(child.leaves())
|
||||
return header_leaves
|
||||
|
||||
|
||||
def _generate_ignored_nodes_from_fmt_skip(
|
||||
leaf: Leaf, comment: ProtoComment, mode: Mode
|
||||
) -> Iterator[LN]:
|
||||
"""Generate all leaves that should be ignored by the `# fmt: skip` from `leaf`."""
|
||||
prev_sibling = leaf.prev_sibling
|
||||
parent = leaf.parent
|
||||
ignored_nodes: list[LN] = []
|
||||
# Need to properly format the leaf prefix to compare it to comment.value,
|
||||
# which is also formatted
|
||||
comments = list_comments(leaf.prefix, is_endmarker=False, mode=mode)
|
||||
if not comments or comment.value != comments[0].value:
|
||||
return
|
||||
|
||||
if not prev_sibling and parent:
|
||||
prev_sibling = parent.prev_sibling
|
||||
|
||||
if prev_sibling is not None:
|
||||
leaf.prefix = leaf.prefix[comment.consumed :]
|
||||
|
||||
# Generates the nodes to be ignored by `fmt: skip`.
|
||||
|
||||
# Nodes to ignore are the ones on the same line as the
|
||||
# `# fmt: skip` comment, excluding the `# fmt: skip`
|
||||
# node itself.
|
||||
|
||||
# Traversal process (starting at the `# fmt: skip` node):
|
||||
# 1. Move to the `prev_sibling` of the current node.
|
||||
# 2. If `prev_sibling` has children, go to its rightmost leaf.
|
||||
# 3. If there's no `prev_sibling`, move up to the parent
|
||||
# node and repeat.
|
||||
# 4. Continue until:
|
||||
# a. You encounter an `INDENT` or `NEWLINE` node (indicates
|
||||
# start of the line).
|
||||
# b. You reach the root node.
|
||||
|
||||
# Include all visited LEAVES in the ignored list, except INDENT
|
||||
# or NEWLINE leaves.
|
||||
|
||||
current_node = prev_sibling
|
||||
ignored_nodes = [current_node]
|
||||
if current_node.prev_sibling is None and current_node.parent is not None:
|
||||
current_node = current_node.parent
|
||||
|
||||
# Track seen nodes to detect cycles that can occur after tree modifications
|
||||
seen_nodes = {id(current_node)}
|
||||
|
||||
while "\n" not in current_node.prefix and current_node.prev_sibling is not None:
|
||||
leaf_nodes = list(current_node.prev_sibling.leaves())
|
||||
next_node = leaf_nodes[-1] if leaf_nodes else current_node
|
||||
|
||||
# Detect infinite loop - if we've seen this node before, stop
|
||||
# This can happen when STANDALONE_COMMENT nodes are inserted
|
||||
# during processing
|
||||
if id(next_node) in seen_nodes:
|
||||
break
|
||||
|
||||
current_node = next_node
|
||||
seen_nodes.add(id(current_node))
|
||||
|
||||
# Stop if we encounter a STANDALONE_COMMENT created by fmt processing
|
||||
if (
|
||||
isinstance(current_node, Leaf)
|
||||
and current_node.type == STANDALONE_COMMENT
|
||||
and hasattr(current_node, "fmt_pass_converted_first_leaf")
|
||||
):
|
||||
break
|
||||
|
||||
if (
|
||||
current_node.type in CLOSING_BRACKETS
|
||||
and current_node.parent
|
||||
and current_node.parent.type == syms.atom
|
||||
):
|
||||
current_node = current_node.parent
|
||||
|
||||
if current_node.type in (token.NEWLINE, token.INDENT):
|
||||
current_node.prefix = ""
|
||||
break
|
||||
|
||||
if current_node.type == token.DEDENT:
|
||||
break
|
||||
|
||||
# Special case for with expressions
|
||||
# Without this, we can stuck inside the asexpr_test's children's children
|
||||
if (
|
||||
current_node.parent
|
||||
and current_node.parent.type == syms.asexpr_test
|
||||
and current_node.parent.parent
|
||||
and current_node.parent.parent.type == syms.with_stmt
|
||||
):
|
||||
current_node = current_node.parent
|
||||
|
||||
ignored_nodes.insert(0, current_node)
|
||||
|
||||
if current_node.prev_sibling is None and current_node.parent is not None:
|
||||
current_node = current_node.parent
|
||||
|
||||
# Special handling for compound statements with semicolon-separated bodies
|
||||
if isinstance(parent, Node):
|
||||
body_node = _find_compound_statement_context(parent)
|
||||
if body_node is not None:
|
||||
header_nodes = _get_compound_statement_header(body_node, parent)
|
||||
if header_nodes:
|
||||
ignored_nodes = header_nodes + ignored_nodes
|
||||
|
||||
yield from ignored_nodes
|
||||
elif (
|
||||
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 = ""
|
||||
parent_sibling = parent.prev_sibling
|
||||
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
|
||||
# grandparent node.
|
||||
grandparent = parent.parent
|
||||
if (
|
||||
grandparent is not None
|
||||
and grandparent.prev_sibling is not None
|
||||
and grandparent.prev_sibling.type == token.ASYNC
|
||||
):
|
||||
ignored_nodes.insert(0, grandparent.prev_sibling)
|
||||
yield from iter(ignored_nodes)
|
||||
|
||||
|
||||
def is_fmt_on(container: LN, mode: Mode) -> bool:
|
||||
"""Determine whether formatting is switched on within a container.
|
||||
Determined by whether the last `# fmt:` comment is `on` or `off`.
|
||||
"""
|
||||
fmt_on = False
|
||||
for comment in list_comments(container.prefix, is_endmarker=False, mode=mode):
|
||||
if contains_fmt_directive(comment.value, FMT_ON):
|
||||
fmt_on = True
|
||||
elif contains_fmt_directive(comment.value, FMT_OFF):
|
||||
fmt_on = False
|
||||
return fmt_on
|
||||
|
||||
|
||||
def children_contains_fmt_on(container: LN, mode: Mode) -> bool:
|
||||
"""Determine if children have formatting switched on."""
|
||||
for child in container.children:
|
||||
leaf = first_leaf_of(child)
|
||||
if leaf is not None and is_fmt_on(leaf, mode=mode):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_pragma_comment(comment_list: list[Leaf]) -> bool:
|
||||
"""
|
||||
Returns:
|
||||
True iff one of the comments in @comment_list is a pragma used by one
|
||||
of the more common static analysis tools for python (e.g. mypy, flake8,
|
||||
pylint).
|
||||
"""
|
||||
for comment in comment_list:
|
||||
if comment.value.startswith(("# type:", "# noqa", "# pylint:")):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_fmt_directive(
|
||||
comment_line: str, directives: set[str] = FMT_OFF | FMT_ON | FMT_SKIP
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the given comment contains format directives, alone or paired with
|
||||
other comments.
|
||||
|
||||
Defaults to checking all directives (skip, off, on, yapf), but can be
|
||||
narrowed to specific ones.
|
||||
|
||||
Matching styles:
|
||||
# foobar <-- single comment
|
||||
# foobar # foobar # foobar <-- multiple comments
|
||||
# foobar; foobar <-- list of comments (; separated)
|
||||
"""
|
||||
semantic_comment_blocks = [
|
||||
comment_line,
|
||||
*[
|
||||
_COMMENT_PREFIX + comment.strip()
|
||||
for comment in comment_line.split(_COMMENT_PREFIX)[1:]
|
||||
],
|
||||
*[
|
||||
_COMMENT_PREFIX + comment.strip()
|
||||
for comment in comment_line.strip(_COMMENT_PREFIX).split(
|
||||
_COMMENT_LIST_SEPARATOR
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
return any(comment in directives for comment in semantic_comment_blocks)
|
||||
Loading…
Add table
Add a link
Reference in a new issue