LibCST/libcst/_nodes/internal.py
Benjamin Woodruff 12b216339b Move CodegenState construction to PositionProvider
Previously, `libcst.Module.code_for_node` accepted a `provider`
parameter, and would construct the appropriate CodegenState subclass
based on some if/else logic.

This had a few knock-on effects:

- A tighter circular dependency between node definitions and metadata,
  which was previously mitigated with an inner import.

- Adding a new `CodegenState` subclass required the non-obvious task of
  modifying `Module`. I'll need to add a new `CodegenState` subclass to
  support incremental codegen.

- What was intended to be a private implementation detail (how positions
  are computed by hooking into codegen) was exposed as a parameter on a
  public method.

This diff aims to clean up those knock on effects. The position-related
subclasses have been moved from `libcst.nodes._internal` into
`libcst.metadata.position_provider`, which keeps more of the position
computation logic together.

Technically this is a breaking change. If somebody was passing the
second parameter into `code_for_node`, their code will break. However:

- It will break in a clear and obvious way.

- This second parameter was never documented (aside from my recent
  addition of some remarks telling people not to use it). There's plenty
  of documentation that shows how to fetch positions properly.

So it's my opinion that we shouldn't require a major version bump for
this change.
2019-10-22 14:53:04 -07:00

192 lines
5.9 KiB
Python

# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# pyre-strict
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Sequence, Union
from libcst._add_slots import add_slots
from libcst._maybe_sentinel import MaybeSentinel
from libcst._position import CodeRange
from libcst._removal_sentinel import RemovalSentinel
from libcst._types import CSTNodeT
if TYPE_CHECKING:
# These are circular dependencies only used for typing purposes
from libcst._nodes.base import CSTNode # noqa: F401
from libcst._visitors import CSTVisitorT
@add_slots
@dataclass(frozen=False)
class CodegenState:
# These are derived from a Module
default_indent: str
default_newline: str
provider: object = None # overridden by libcst.metadata.position_provider
indent_tokens: List[str] = field(default_factory=list)
tokens: List[str] = field(default_factory=list)
line: int = 1 # one-indexed
column: int = 0 # zero-indexed
def increase_indent(self, value: str) -> None:
self.indent_tokens.append(value)
def decrease_indent(self) -> None:
self.indent_tokens.pop()
def add_indent_tokens(self) -> None:
self.tokens.extend(self.indent_tokens)
def add_token(self, value: str) -> None:
self.tokens.append(value)
def record_position(self, node: "CSTNode", position: CodeRange) -> None:
pass
@contextmanager
def record_syntactic_position(
self,
node: "CSTNode",
*,
start_node: Optional["CSTNode"] = None,
end_node: Optional["CSTNode"] = None,
) -> Iterator[None]:
yield
def visit_required(
parent: "CSTNode", fieldname: str, node: CSTNodeT, visitor: "CSTVisitorT"
) -> CSTNodeT:
"""
Given a node, visits the node using `visitor`. If removal is attempted by the
visitor, an exception is raised.
"""
visitor.on_visit_attribute(parent, fieldname)
result = node.visit(visitor)
if isinstance(result, RemovalSentinel):
raise TypeError(
f"We got a RemovalSentinel while visiting a {type(node).__name__}. This "
+ "node's parent does not allow it to be removed."
)
visitor.on_leave_attribute(parent, fieldname)
return result
def visit_optional(
parent: "CSTNode", fieldname: str, node: Optional[CSTNodeT], visitor: "CSTVisitorT"
) -> Optional[CSTNodeT]:
"""
Given an optional node, visits the node if it exists with `visitor`. If the node is
removed, returns None.
"""
if node is None:
visitor.on_visit_attribute(parent, fieldname)
visitor.on_leave_attribute(parent, fieldname)
return None
visitor.on_visit_attribute(parent, fieldname)
result = node.visit(visitor)
visitor.on_leave_attribute(parent, fieldname)
return None if isinstance(result, RemovalSentinel) else result
def visit_sentinel(
parent: "CSTNode",
fieldname: str,
node: Union[CSTNodeT, MaybeSentinel],
visitor: "CSTVisitorT",
) -> Union[CSTNodeT, MaybeSentinel]:
"""
Given a node that can be a real value or a sentinel value, visits the node if it
is real with `visitor`. If the node is removed, returns MaybeSentinel.
"""
if isinstance(node, MaybeSentinel):
visitor.on_visit_attribute(parent, fieldname)
visitor.on_leave_attribute(parent, fieldname)
return MaybeSentinel.DEFAULT
visitor.on_visit_attribute(parent, fieldname)
result = node.visit(visitor)
visitor.on_leave_attribute(parent, fieldname)
return MaybeSentinel.DEFAULT if isinstance(result, RemovalSentinel) else result
def visit_iterable(
parent: "CSTNode",
fieldname: str,
children: Iterable[CSTNodeT],
visitor: "CSTVisitorT",
) -> Iterable[CSTNodeT]:
"""
Given an iterable of children, visits each child with `visitor`, and yields the new
children with any `RemovalSentinel` values removed.
"""
visitor.on_visit_attribute(parent, fieldname)
for child in children:
new_child = child.visit(visitor)
if not isinstance(new_child, RemovalSentinel):
yield new_child
visitor.on_leave_attribute(parent, fieldname)
def visit_sequence(
parent: "CSTNode",
fieldname: str,
children: Sequence[CSTNodeT],
visitor: "CSTVisitorT",
) -> Sequence[CSTNodeT]:
"""
A convenience wrapper for `visit_iterable` that returns a sequence instead of an
iterable.
"""
return tuple(visit_iterable(parent, fieldname, children, visitor))
def visit_body_iterable(
parent: "CSTNode",
fieldname: str,
children: Sequence[CSTNodeT],
visitor: "CSTVisitorT",
) -> Iterable[CSTNodeT]:
"""
Similar to visit_iterable above, but capable of discarding empty SimpleStatementLine
nodes in order to preserve correct pass insertion behavior.
"""
visitor.on_visit_attribute(parent, fieldname)
for child in children:
new_child = child.visit(visitor)
# Don't yield a child if we removed it.
if isinstance(new_child, RemovalSentinel):
continue
# Don't yield a child if the old child wasn't empty
# and the new child is. This means a RemovalSentinel
# caused a child of this node to be dropped, and it
# is now useless.
if (not child._is_removable()) and new_child._is_removable():
continue
# Safe to yield child in this case.
yield new_child
visitor.on_leave_attribute(parent, fieldname)
def visit_body_sequence(
parent: "CSTNode",
fieldname: str,
children: Sequence[CSTNodeT],
visitor: "CSTVisitorT",
) -> Sequence[CSTNodeT]:
"""
A convenience wrapper for `visit_body_iterable` that returns a sequence
instead of an iterable.
"""
return tuple(visit_body_iterable(parent, fieldname, children, visitor))