LibCST/libcst/nodes/_expression.py
Benjamin Woodruff a293787f8c Add node class and parser implementation for List
This is based heavily on the implementation of Tuple, and was pretty
straightforward as a result.
2019-07-22 19:53:49 -07:00

2258 lines
80 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
import re
from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum, auto
from tokenize import (
Floatnumber as FLOATNUMBER_RE,
Imagnumber as IMAGNUMBER_RE,
Intnumber as INTNUMBER_RE,
)
from typing import Callable, Generator, Optional, Sequence, Union
from typing_extensions import Literal
from libcst._add_slots import add_slots
from libcst._base_visitor import CSTVisitor
from libcst._maybe_sentinel import MaybeSentinel
from libcst.nodes._base import (
AnnotationIndicatorSentinel,
CSTCodegenError,
CSTNode,
CSTValidationError,
)
from libcst.nodes._internal import (
CodegenState,
visit_optional,
visit_required,
visit_sentinel,
visit_sequence,
)
from libcst.nodes._op import (
AssignEqual,
BaseBinaryOp,
BaseBooleanOp,
BaseCompOp,
BaseUnaryOp,
Colon,
Comma,
Dot,
In,
Is,
IsNot,
Minus,
Not,
NotIn,
Plus,
)
from libcst.nodes._whitespace import BaseParenthesizableWhitespace, SimpleWhitespace
@add_slots
@dataclass(frozen=True)
class LeftSquareBracket(CSTNode):
"""
Used by various nodes to denote a subscript or list section. This doesn't own
the whitespace to the left of it since this is owned by the parent node.
"""
whitespace_after: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "LeftSquareBracket":
return LeftSquareBracket(
whitespace_after=visit_required(
"whitespace_after", self.whitespace_after, visitor
)
)
def _codegen_impl(self, state: CodegenState) -> None:
state.add_token("[")
self.whitespace_after._codegen(state)
@add_slots
@dataclass(frozen=True)
class RightSquareBracket(CSTNode):
"""
Used by various nodes to denote a subscript or list section. This doesn't own
the whitespace to the right of it since this is owned by the parent node.
"""
whitespace_before: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "RightSquareBracket":
return RightSquareBracket(
whitespace_before=visit_required(
"whitespace_before", self.whitespace_before, visitor
)
)
def _codegen_impl(self, state: CodegenState) -> None:
self.whitespace_before._codegen(state)
state.add_token("]")
@add_slots
@dataclass(frozen=True)
class LeftParen(CSTNode):
"""
Used by various nodes to denote a parenthesized section. This doesn't own
the whitespace to the left of it since this is owned by the parent node.
"""
whitespace_after: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "LeftParen":
return LeftParen(
whitespace_after=visit_required(
"whitespace_after", self.whitespace_after, visitor
)
)
def _codegen_impl(self, state: CodegenState) -> None:
state.add_token("(")
self.whitespace_after._codegen(state)
@add_slots
@dataclass(frozen=True)
class RightParen(CSTNode):
"""
Used by various nodes to denote a parenthesized section. This doesn't own
the whitespace to the right of it since this is owned by the parent node.
"""
whitespace_before: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "RightParen":
return RightParen(
whitespace_before=visit_required(
"whitespace_before", self.whitespace_before, visitor
)
)
def _codegen_impl(self, state: CodegenState) -> None:
self.whitespace_before._codegen(state)
state.add_token(")")
class _BaseParenthesizedNode(CSTNode, ABC):
"""
We don't want to have another level of indirection for parenthesis in
our tree, since that makes us more of a CST than an AST. So, all the
expressions or atoms that can be wrapped in parenthesis will subclass
this to get that functionality.
"""
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
if self.lpar and not self.rpar:
raise CSTValidationError("Cannot have left paren without right paren.")
if not self.lpar and self.rpar:
raise CSTValidationError("Cannot have right paren without left paren.")
if len(self.lpar) != len(self.rpar):
raise CSTValidationError("Cannot have unbalanced parens.")
@contextmanager
def _parenthesize(self, state: CodegenState) -> Generator[None, None, None]:
for lpar in self.lpar:
lpar._codegen(state)
with state.record_syntactic_position(self):
yield
for rpar in self.rpar:
rpar._codegen(state)
class ExpressionPosition(Enum):
LEFT = auto()
RIGHT = auto()
class BaseExpression(_BaseParenthesizedNode, ABC):
def _safe_to_use_with_word_operator(self, position: ExpressionPosition) -> bool:
"""
Returns true if this expression is safe to be use with a word operator
such as "not" without space between the operator an ourselves. Examples
where this is true are "not(True)", "(1)in[1,2,3]", etc. This base
function handles parenthesized nodes, but certain nodes such as tuples,
dictionaries and lists will override this to signifiy that they're always
safe.
"""
return len(self.lpar) > 0 and len(self.rpar) > 0
class BaseAtom(BaseExpression, ABC):
"""
> Atoms are the most basic elements of expressions. The simplest atoms are
> identifiers or literals. Forms enclosed in parentheses, brackets or braces are
> also categorized syntactically as atoms.
-- https://docs.python.org/3/reference/expressions.html#atoms
"""
pass
class BaseAssignTargetExpression(BaseExpression, ABC):
"""
An expression that's valid on the left side of an assign statement.
Python's grammar defines all expression as valid in this position, but the AST
compiler further restricts the allowed types, which is what this type attempts to
express.
See also: https://github.com/python/cpython/blob/v3.8.0a4/Python/ast.c#L1120
"""
pass
class BaseDelTargetExpression(BaseExpression, ABC):
"""
An expression that's valid on the right side of a 'del' statement.
Python's grammar defines all expression as valid in this position, but the AST
compiler further restricts the allowed types, which is what this type attempts to
express.
This is similar to a BaseAssignTargetExpression, but excludes `Starred`.
See also: https://github.com/python/cpython/blob/v3.8.0a4/Python/ast.c#L1120
and: https://github.com/python/cpython/blob/v3.8.0a4/Python/compile.c#L4854
"""
pass
@add_slots
@dataclass(frozen=True)
class Name(BaseAssignTargetExpression, BaseDelTargetExpression, BaseAtom):
# The actual identifier string
value: str
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Name":
return Name(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=self.value,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _validate(self) -> None:
super(Name, self)._validate()
if len(self.value) == 0:
raise CSTValidationError("Cannot have empty name identifier.")
if not self.value.isidentifier():
raise CSTValidationError("Name is not a valid identifier.")
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.value)
@add_slots
@dataclass(frozen=True)
class Ellipses(BaseAtom):
"""
An ellipses "..."
"""
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Ellipses":
return Ellipses(
lpar=visit_sequence("lpar", self.lpar, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token("...")
@add_slots
@dataclass(frozen=True)
class Integer(_BaseParenthesizedNode):
value: str
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Integer":
return Integer(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=self.value,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _validate(self) -> None:
super(Integer, self)._validate()
if not re.fullmatch(INTNUMBER_RE, self.value):
raise CSTValidationError("Number is not a valid integer.")
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.value)
@add_slots
@dataclass(frozen=True)
class Float(_BaseParenthesizedNode):
value: str
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Float":
return Float(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=self.value,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _validate(self) -> None:
super(Float, self)._validate()
if not re.fullmatch(FLOATNUMBER_RE, self.value):
raise CSTValidationError("Number is not a valid float.")
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.value)
@add_slots
@dataclass(frozen=True)
class Imaginary(_BaseParenthesizedNode):
value: str
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Imaginary":
return Imaginary(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=self.value,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _validate(self) -> None:
super(Imaginary, self)._validate()
if not re.fullmatch(IMAGNUMBER_RE, self.value):
raise CSTValidationError("Number is not a valid imaginary.")
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.value)
@add_slots
@dataclass(frozen=True)
class Number(BaseAtom):
# The actual number component
number: Union[Integer, Float, Imaginary]
# Any unary operator applied to the number
operator: Optional[Union[Plus, Minus]] = None
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _safe_to_use_with_word_operator(self, position: ExpressionPosition) -> bool:
"""
Numbers are funny. The expression "5in [1,2,3,4,5]" is a valid expression
which evaluates to "True". So, encapsulate that here by allowing zero spacing
with the left hand side of an expression with a comparison operator.
"""
if position == ExpressionPosition.LEFT:
return True
return super(Number, self)._safe_to_use_with_word_operator(position)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Number":
return Number(
lpar=visit_sequence("lpar", self.lpar, visitor),
operator=visit_optional("operator", self.operator, visitor),
number=visit_required("number", self.number, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
# TODO: blocked until parsing numbers is fixed
# TODO: handle cases involving parentheses
# ex: "((1))" "(+(1))"
with self._parenthesize(state):
operator = self.operator
if operator is not None:
operator._codegen(state)
self.number._codegen(state)
class BaseString(BaseAtom, ABC):
"""
A type that can be used anywhere that you need to explicitly take any
string.
"""
pass
@add_slots
@dataclass(frozen=True)
class SimpleString(BaseString):
value: str
# Sequence of open parenthesis for precidence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precidence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
super(SimpleString, self)._validate()
# Validate any prefix
prefix = self._get_prefix()
if prefix not in ("", "r", "u", "b", "br", "rb"):
raise CSTValidationError("Invalid string prefix.")
prefixlen = len(prefix)
# Validate wrapping quotes
if len(self.value) < (prefixlen + 2):
raise CSTValidationError("String must have enclosing quotes.")
if (
self.value[prefixlen] not in ['"', "'"]
or self.value[prefixlen] != self.value[-1]
):
raise CSTValidationError("String must have matching enclosing quotes.")
# Check validity of triple-quoted strings
if len(self.value) >= (prefixlen + 6):
if self.value[prefixlen] == self.value[prefixlen + 1]:
# We know this isn't an empty string, so there needs to be a third
# identical enclosing token.
if (
self.value[prefixlen] != self.value[prefixlen + 2]
or self.value[prefixlen] != self.value[-2]
or self.value[prefixlen] != self.value[-3]
):
raise CSTValidationError(
"String must have matching enclosing quotes."
)
# We should check the contents as well, but this is pretty complicated,
# partially due to triple-quoted strings.
def _get_prefix(self) -> str:
prefix = ""
for c in self.value:
if c in ['"', "'"]:
break
prefix += c
return prefix.lower()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "SimpleString":
return SimpleString(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=self.value,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.value)
class BaseFormattedStringContent(CSTNode, ABC):
"""
A type that can be used anywhere that you need to take any part of a f-string.
"""
pass
@add_slots
@dataclass(frozen=True)
class FormattedStringText(BaseFormattedStringContent):
# The raw string value.
value: str
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "FormattedStringText":
return FormattedStringText(value=self.value)
def _codegen_impl(self, state: CodegenState) -> None:
state.add_token(self.value)
@add_slots
@dataclass(frozen=True)
class FormattedStringExpression(BaseFormattedStringContent):
# The expression we will render when printing the string
expression: BaseExpression
# An optional conversion specifier
conversion: Optional[str] = None
# An optional format specifier
format_spec: Optional[Sequence[BaseFormattedStringContent]] = None
# Whitespace
whitespace_before_expression: BaseParenthesizableWhitespace = SimpleWhitespace("")
whitespace_after_expression: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _validate(self) -> None:
if self.conversion is not None and self.conversion not in ("s", "r", "a"):
raise CSTValidationError("Invalid f-string conversion.")
def _visit_and_replace_children(
self, visitor: CSTVisitor
) -> "FormattedStringExpression":
format_spec = self.format_spec
return FormattedStringExpression(
whitespace_before_expression=visit_required(
"whitespace_before_expression",
self.whitespace_before_expression,
visitor,
),
expression=visit_required("expression", self.expression, visitor),
whitespace_after_expression=visit_required(
"whitespace_after_expression", self.whitespace_after_expression, visitor
),
conversion=self.conversion,
format_spec=(
visit_sequence("format_spec", format_spec, visitor)
if format_spec is not None
else None
),
)
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
state.add_token("{")
self.whitespace_before_expression._codegen(state)
self.expression._codegen(state)
self.whitespace_after_expression._codegen(state)
conversion = self.conversion
if conversion is not None:
state.add_token("!")
state.add_token(conversion)
format_spec = self.format_spec
if format_spec is not None:
state.add_token(":")
for spec in format_spec:
spec._codegen(state)
state.add_token("}")
@add_slots
@dataclass(frozen=True)
class FormattedString(BaseString):
# Sequence of formatted string parts
parts: Sequence[BaseFormattedStringContent]
# String start indicator
start: str = 'f"'
# String end indicator
end: str = '"'
# Sequence of open parenthesis for precidence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precidence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
super(FormattedString, self)._validate()
# Validate any prefix
prefix = self._get_prefix()
if prefix not in ("f", "fr", "rf"):
raise CSTValidationError("Invalid f-string prefix.")
# Validate wrapping quotes
starttoken = self.start[len(prefix) :]
if starttoken != self.end:
raise CSTValidationError("f-string must have matching enclosing quotes.")
# Validate valid wrapping quote usage
if starttoken not in ('"', "'", '"""', "'''"):
raise CSTValidationError("Invalid f-string enclosing quotes.")
def _get_prefix(self) -> str:
prefix = ""
for c in self.start:
if c in ['"', "'"]:
break
prefix += c
return prefix.lower()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "FormattedString":
return FormattedString(
lpar=visit_sequence("lpar", self.lpar, visitor),
start=self.start,
parts=visit_sequence("parts", self.parts, visitor),
end=self.end,
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token(self.start)
for part in self.parts:
part._codegen(state)
state.add_token(self.end)
@add_slots
@dataclass(frozen=True)
class ConcatenatedString(BaseString):
# String on the left of the concatenation.
left: Union[SimpleString, FormattedString]
# String on the right of the concatenation.
right: Union[SimpleString, FormattedString, "ConcatenatedString"]
# Sequence of open parenthesis for precidence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precidence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace between strings.
whitespace_between: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _validate(self) -> None:
super(ConcatenatedString, self)._validate()
# Strings that are concatenated cannot have parens.
if bool(self.left.lpar) or bool(self.left.rpar):
raise CSTValidationError("Cannot concatenate parenthesized strings.")
if bool(self.right.lpar) or bool(self.right.rpar):
raise CSTValidationError("Cannot concatenate parenthesized strings.")
# Cannot concatenate str and bytes
leftbytes = "b" in self.left._get_prefix()
if isinstance(self.right, ConcatenatedString):
rightbytes = "b" in self.right.left._get_prefix()
elif isinstance(self.right, SimpleString):
rightbytes = "b" in self.right._get_prefix()
elif isinstance(self.right, FormattedString):
rightbytes = "b" in self.right._get_prefix()
else:
raise Exception("Logic error!")
if leftbytes != rightbytes:
raise CSTValidationError("Cannot concatenate string and bytes.")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "ConcatenatedString":
return ConcatenatedString(
lpar=visit_sequence("lpar", self.lpar, visitor),
left=visit_required("left", self.left, visitor),
whitespace_between=visit_required(
"whitespace_between", self.whitespace_between, visitor
),
right=visit_required("right", self.right, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.left._codegen(state)
self.whitespace_between._codegen(state)
self.right._codegen(state)
@add_slots
@dataclass(frozen=True)
class ComparisonTarget(CSTNode):
"""
A target for a comparison. Owns the comparison operator itself.
"""
# The actual comparison operator
operator: BaseCompOp
# The right hand side of the comparison operation
comparator: BaseExpression
def _validate(self) -> None:
# Validate operator spacing rules
if (
isinstance(self.operator, (In, NotIn, Is, IsNot))
and self.operator.whitespace_after.empty
and not self.comparator._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError(
"Must have at least one space around comparison operator."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "ComparisonTarget":
return ComparisonTarget(
operator=visit_required("operator", self.operator, visitor),
comparator=visit_required("comparator", self.comparator, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
self.operator._codegen(state)
self.comparator._codegen(state)
@add_slots
@dataclass(frozen=True)
class Comparison(BaseExpression):
"""
Any comparison such as "x < y < z"
"""
# The left hand side of the comparison operation
left: BaseExpression
# The actual comparison operator
comparisons: Sequence[ComparisonTarget]
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
# Perform any validation on base type
super(Comparison, self)._validate()
if len(self.comparisons) == 0:
raise CSTValidationError("Must have at least one ComparisonTarget.")
# Validate operator spacing rules
operator = self.comparisons[0].operator
if (
isinstance(operator, (In, NotIn, Is, IsNot))
and operator.whitespace_before.empty
and not self.left._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space around comparison operator."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Comparison":
return Comparison(
lpar=visit_sequence("lpar", self.lpar, visitor),
left=visit_required("left", self.left, visitor),
comparisons=visit_sequence("comparisons", self.comparisons, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.left._codegen(state)
for comp in self.comparisons:
comp._codegen(state)
@add_slots
@dataclass(frozen=True)
class UnaryOperation(BaseExpression):
"""
Any generic unary expression, such as "not x" or "-x". Note that this node
does not get used for immediate number negation such as "-5". For that,
the Number class is used.
"""
# The unary operator applied to the expression
operator: BaseUnaryOp
# The actual expression or atom
expression: BaseExpression
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
# Perform any validation on base type
super(UnaryOperation, self)._validate()
if (
isinstance(self.operator, Not)
and self.operator.whitespace_after.empty
and not self.expression._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError("Must have at least one space after not operator.")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "UnaryOperation":
return UnaryOperation(
lpar=visit_sequence("lpar", self.lpar, visitor),
operator=visit_required("operator", self.operator, visitor),
expression=visit_required("expression", self.expression, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.operator._codegen(state)
self.expression._codegen(state)
@add_slots
@dataclass(frozen=True)
class BinaryOperation(BaseExpression):
"""
Any binary operation such as "x << y" or "y + z".
"""
# The left hand side of the operation
left: BaseExpression
# The actual operator
operator: BaseBinaryOp
# The right hand side of the operation
right: BaseExpression
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "BinaryOperation":
return BinaryOperation(
lpar=visit_sequence("lpar", self.lpar, visitor),
left=visit_required("left", self.left, visitor),
operator=visit_required("operator", self.operator, visitor),
right=visit_required("right", self.right, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.left._codegen(state)
self.operator._codegen(state)
self.right._codegen(state)
@add_slots
@dataclass(frozen=True)
class BooleanOperation(BaseExpression):
"""
Any boolean operation such as "x or y" or "z and w"
"""
# The left hand side of the operation
left: BaseExpression
# The actual operator
operator: BaseBooleanOp
# The right hand side of the operation
right: BaseExpression
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
# Paren validation and such
super(BooleanOperation, self)._validate()
# Validate spacing rules
if (
self.operator.whitespace_before.empty
and not self.left._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space around boolean operator."
)
if (
self.operator.whitespace_after.empty
and not self.right._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError(
"Must have at least one space around boolean operator."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "BooleanOperation":
return BooleanOperation(
lpar=visit_sequence("lpar", self.lpar, visitor),
left=visit_required("left", self.left, visitor),
operator=visit_required("operator", self.operator, visitor),
right=visit_required("right", self.right, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.left._codegen(state)
self.operator._codegen(state)
self.right._codegen(state)
@dataclass(frozen=True)
class Attribute(BaseAssignTargetExpression, BaseDelTargetExpression):
"""
An attribute reference, such as "x.y". Note that in the case of
"x.y.z", the outer attribute will have an attr of "z" and the
value will be another Attribute referencing the "y" attribute on
"x".
"""
# Expression which, when evaluated, will have 'attr' as an attribute
value: BaseExpression
# Name of the attribute being accessed.
attr: Name
# Separating dot, with any whitespace it owns.
dot: Dot = Dot()
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Attribute":
return Attribute(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=visit_required("value", self.value, visitor),
dot=visit_required("dot", self.dot, visitor),
attr=visit_required("attr", self.attr, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.value._codegen(state)
self.dot._codegen(state)
self.attr._codegen(state)
@add_slots
@dataclass(frozen=True)
class Index(CSTNode):
"""
Any index as passed to a subscript.
"""
# The index value itself.
value: BaseExpression
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Index":
return Index(value=visit_required("value", self.value, visitor))
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
@add_slots
@dataclass(frozen=True)
class Slice(CSTNode):
"""
Any slice operation in a subscript, such as "1:", "2:3:4", etc. Note
that the grammar does NOT allow parenthesis around a slice so they
are not supported here.
"""
# The lower bound in the slice, if present
lower: Optional[BaseExpression]
# The upper bound in the slice, if present
upper: Optional[BaseExpression]
# The step in the slice, if present
step: Optional[BaseExpression] = None
# The first slice operator
first_colon: Colon = Colon()
# The second slice operator, usually omitted
second_colon: Union[Colon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Slice":
return Slice(
lower=visit_optional("lower", self.lower, visitor),
first_colon=visit_required("first_colon", self.first_colon, visitor),
upper=visit_optional("upper", self.upper, visitor),
second_colon=visit_sentinel("second_colon", self.second_colon, visitor),
step=visit_optional("step", self.step, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
lower = self.lower
if lower is not None:
lower._codegen(state)
self.first_colon._codegen(state)
upper = self.upper
if upper is not None:
upper._codegen(state)
second_colon = self.second_colon
if second_colon is MaybeSentinel.DEFAULT and self.step is not None:
state.add_token(":")
elif isinstance(second_colon, Colon):
second_colon._codegen(state)
step = self.step
if step is not None:
step._codegen(state)
@add_slots
@dataclass(frozen=True)
class ExtSlice(CSTNode):
"""
A list of slices, such as "1:2, 3". Not used in the stdlib but still
valid. This also does not allow for wrapping parenthesis.
"x".
"""
# A slice or index that is part of the extslice.
slice: Union[Index, Slice]
# Separating comma, with any whitespace it owns.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "ExtSlice":
return ExtSlice(
slice=visit_required("slice", self.slice, visitor),
comma=visit_sentinel("comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.slice._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class Subscript(BaseAssignTargetExpression, BaseDelTargetExpression):
"""
A subscript reference such as "x[2]".
"""
# Expression which, when evaluated, will be subscripted.
value: BaseExpression
# Subscript to take on the value.
slice: Union[Index, Slice, Sequence[ExtSlice]]
# Open bracket surrounding the slice
lbracket: LeftSquareBracket = LeftSquareBracket()
# Close bracket surrounding the slice
rbracket: RightSquareBracket = RightSquareBracket()
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace
whitespace_after_value: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _validate(self) -> None:
super(Subscript, self)._validate()
if isinstance(self.slice, Sequence):
# Validate valid commas
if len(self.slice) < 1:
raise CSTValidationError("Cannot have empty ExtSlice.")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Subscript":
slice = self.slice
return Subscript(
lpar=visit_sequence("lpar", self.lpar, visitor),
value=visit_required("value", self.value, visitor),
whitespace_after_value=visit_required(
"whitespace_after_value", self.whitespace_after_value, visitor
),
lbracket=visit_required("lbracket", self.lbracket, visitor),
slice=visit_required("slice", slice, visitor)
if isinstance(slice, (Index, Slice))
else visit_sequence("slice", slice, visitor),
rbracket=visit_required("rbracket", self.rbracket, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.value._codegen(state)
self.whitespace_after_value._codegen(state)
self.lbracket._codegen(state)
if isinstance(self.slice, (Index, Slice)):
self.slice._codegen(state)
elif isinstance(self.slice, Sequence):
lastslice = len(self.slice) - 1
for i, slice in enumerate(self.slice):
slice._codegen(state, default_comma=(i != lastslice))
else:
# We can make pyre happy this way!
raise Exception("Logic error!")
self.rbracket._codegen(state)
@add_slots
@dataclass(frozen=True)
class Annotation(CSTNode):
"""
An annotation.
"""
# The annotation itself.
annotation: Union[Name, Attribute, BaseString, Subscript]
# The indicator token before the annotation.
indicator: Union[
str, AnnotationIndicatorSentinel
] = AnnotationIndicatorSentinel.DEFAULT
# Whitespace
whitespace_before_indicator: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
whitespace_after_indicator: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
def _validate(self) -> None:
if isinstance(self.indicator, str) and self.indicator not in [":", "->"]:
raise CSTValidationError(
"An Annotation indicator must be one of ':', '->'."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Annotation":
return Annotation(
whitespace_before_indicator=visit_sentinel(
"whitespace_before_indicator", self.whitespace_before_indicator, visitor
),
indicator=self.indicator,
whitespace_after_indicator=visit_required(
"whitespace_after_indicator", self.whitespace_after_indicator, visitor
),
annotation=visit_required("annotation", self.annotation, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_indicator: Optional[str] = None
) -> None:
# First, figure out the indicator which tells us default whitespace.
indicator = self.indicator
if isinstance(indicator, AnnotationIndicatorSentinel):
if default_indicator is None:
raise CSTCodegenError(
"Must specify a concrete default_indicator if default used on indicator."
)
indicator = default_indicator
# Now, output the whitespace
whitespace_before_indicator = self.whitespace_before_indicator
if isinstance(whitespace_before_indicator, BaseParenthesizableWhitespace):
whitespace_before_indicator._codegen(state)
elif isinstance(whitespace_before_indicator, MaybeSentinel):
if indicator == "->":
state.add_token(" ")
else:
raise Exception("Logic error!")
# Now, output the indicator and the rest of the annotation
state.add_token(indicator)
self.whitespace_after_indicator._codegen(state)
with state.record_syntactic_position(self):
self.annotation._codegen(state)
@add_slots
@dataclass(frozen=True)
class ParamStar(CSTNode):
"""
A sentinel indicator on a Parameter list to denote that the following params
are kwonly args.
"""
# Comma that comes after the star.
comma: Comma = Comma(whitespace_after=SimpleWhitespace(" "))
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "ParamStar":
return ParamStar(comma=visit_required("comma", self.comma, visitor))
def _codegen_impl(self, state: CodegenState) -> None:
state.add_token("*")
self.comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class Param(CSTNode):
"""
A single parameter in a Parameter list. May contain a type annotation and
in some cases a default.
"""
# The parameter name itself
name: Name
# Any optional annotation
annotation: Optional[Annotation] = None
# The equals sign used to denote assignment if there is a default.
equal: Union[AssignEqual, MaybeSentinel] = MaybeSentinel.DEFAULT
# Any optional default
default: Optional[BaseExpression] = None
# Any trailing comma
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
# Optional star appearing before name for star_arg and star_kwarg
star: Union[str, MaybeSentinel] = MaybeSentinel.DEFAULT
# Whitespace
whitespace_after_star: BaseParenthesizableWhitespace = SimpleWhitespace("")
whitespace_after_param: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _validate(self) -> None:
if self.default is None and isinstance(self.equal, AssignEqual):
raise CSTValidationError(
"Must have a default when specifying an AssignEqual."
)
if isinstance(self.star, str) and self.star not in ("", "*", "**"):
raise CSTValidationError("Must specify either '', '*' or '**' for star.")
if (
self.annotation is not None
and isinstance(self.annotation.indicator, str)
and self.annotation.indicator != ":"
):
raise CSTValidationError("A param Annotation must be denoted with a ':'.")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Param":
return Param(
star=self.star,
whitespace_after_star=visit_required(
"whitespace_after_star", self.whitespace_after_star, visitor
),
name=visit_required("name", self.name, visitor),
annotation=visit_optional("annotation", self.annotation, visitor),
equal=visit_sentinel("equal", self.equal, visitor),
default=visit_optional("default", self.default, visitor),
comma=visit_sentinel("comma", self.comma, visitor),
whitespace_after_param=visit_required(
"whitespace_after_param", self.whitespace_after_param, visitor
),
)
def _codegen_impl(
self,
state: CodegenState,
default_star: Optional[str] = None,
default_comma: bool = False,
) -> None:
with state.record_syntactic_position(self):
star = self.star
if isinstance(star, MaybeSentinel):
if default_star is None:
raise CSTCodegenError(
"Must specify a concrete default_star if default used on star."
)
star = default_star
if isinstance(star, str):
state.add_token(star)
self.whitespace_after_star._codegen(state)
self.name._codegen(state)
annotation = self.annotation
if annotation is not None:
annotation._codegen(state, default_indicator=":")
equal = self.equal
if equal is MaybeSentinel.DEFAULT and self.default is not None:
state.add_token(" = ")
elif isinstance(equal, AssignEqual):
equal._codegen(state)
default = self.default
if default is not None:
default._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
self.whitespace_after_param._codegen(state)
@add_slots
@dataclass(frozen=True)
class Parameters(CSTNode):
"""
A function or lambda parameter list.
"""
# Positional parameters.
params: Sequence[Param] = ()
# Positional parameters with defaults.
default_params: Sequence[Param] = ()
# Optional parameter that captures unspecified positional arguments or a sentinel
# star that dictates parameters following are kwonly args.
star_arg: Union[Param, ParamStar, MaybeSentinel] = MaybeSentinel.DEFAULT
# Keyword-only params that may or may not have defaults.
kwonly_params: Sequence[Param] = ()
# Optional parameter that captures unspecified kwargs.
star_kwarg: Optional[Param] = None
def _validate_stars_sequence(self, vals: Sequence[Param], *, section: str) -> None:
if len(vals) == 0:
return
for val in vals:
if isinstance(val.star, str) and val.star != "":
raise CSTValidationError(
f"Expecting a star prefix of '' for {section} Param."
)
def _validate_kwonlystar(self) -> None:
if isinstance(self.star_arg, ParamStar) and len(self.kwonly_params) == 0:
raise CSTValidationError(
"Must have at least one kwonly param if ParamStar is used."
)
def _validate_defaults(self) -> None:
for param in self.params:
if param.default is not None:
raise CSTValidationError(
"Cannot have defaults for params. Place them in default_params."
)
for param in self.default_params:
if param.default is None:
raise CSTValidationError(
"Must have defaults for default_params. Place non-defaults in params."
)
if isinstance(self.star_arg, Param) and self.star_arg.default is not None:
raise CSTValidationError("Cannot have default for star_arg.")
if self.star_kwarg is not None and self.star_kwarg.default is not None:
raise CSTValidationError("Cannot have default for star_kwarg.")
def _validate_stars(self) -> None:
if len(self.params) > 0:
self._validate_stars_sequence(self.params, section="params")
if len(self.default_params) > 0:
self._validate_stars_sequence(self.default_params, section="default_params")
star_arg = self.star_arg
if (
isinstance(star_arg, Param)
and isinstance(star_arg.star, str)
and star_arg.star != "*"
):
raise CSTValidationError(
"Expecting a star prefix of '*' for star_arg Param."
)
if len(self.kwonly_params) > 0:
self._validate_stars_sequence(self.kwonly_params, section="kwonly_params")
star_kwarg = self.star_kwarg
if (
star_kwarg is not None
and isinstance(star_kwarg.star, str)
and star_kwarg.star != "**"
):
raise CSTValidationError(
"Expecting a star prefix of '**' for star_kwarg Param."
)
def _validate(self) -> None:
# Validate kwonly_param star placement semantics.
self._validate_kwonlystar()
# Validate defaults semantics for params, default_params and star_arg/star_kwarg.
self._validate_defaults()
# Validate that we don't have random stars on non star_kwarg.
self._validate_stars()
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Parameters":
return Parameters(
params=visit_sequence("params", self.params, visitor),
default_params=visit_sequence(
"default_params", self.default_params, visitor
),
star_arg=visit_sentinel("star_arg", self.star_arg, visitor),
kwonly_params=visit_sequence("kwonly_params", self.kwonly_params, visitor),
star_kwarg=visit_optional("star_kwarg", self.star_kwarg, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
# TODO: remove this when fallback to syntactic whitespace becomes available
with state.record_syntactic_position(self):
# Compute the star existence first so we can ask about whether
# each element is the last in the list or not.
star_arg = self.star_arg
if isinstance(star_arg, MaybeSentinel):
starincluded = len(self.kwonly_params) > 0
elif isinstance(star_arg, (Param, ParamStar)):
starincluded = True
else:
starincluded = False
# Render out the params first, computing necessary trailing commas.
lastparam = len(self.params) - 1
more_values = (
len(self.default_params) > 0
or starincluded
or len(self.kwonly_params) > 0
or self.star_kwarg is not None
)
for i, param in enumerate(self.params):
param._codegen(
state, default_star="", default_comma=(i < lastparam or more_values)
)
# Render out the default_params next, computing necessary trailing commas.
lastparam = len(self.default_params) - 1
more_values = (
starincluded
or len(self.kwonly_params) > 0
or self.star_kwarg is not None
)
for i, param in enumerate(self.default_params):
param._codegen(
state, default_star="", default_comma=(i < lastparam or more_values)
)
# Render out optional star sentinel if its explicitly included or
# if we are inferring it from kwonly_params. Otherwise, render out the
# optional star_arg.
if isinstance(star_arg, MaybeSentinel):
if starincluded:
state.add_token("*, ")
elif isinstance(star_arg, Param):
more_values = len(self.kwonly_params) > 0 or self.star_kwarg is not None
star_arg._codegen(state, default_star="*", default_comma=more_values)
elif isinstance(star_arg, ParamStar):
star_arg._codegen(state)
# Render out the kwonly_args next, computing necessary trailing commas.
lastparam = len(self.kwonly_params) - 1
more_values = self.star_kwarg is not None
for i, param in enumerate(self.kwonly_params):
param._codegen(
state, default_star="", default_comma=(i < lastparam or more_values)
)
# Finally, render out any optional star_kwarg
star_kwarg = self.star_kwarg
if star_kwarg is not None:
star_kwarg._codegen(state, default_star="**", default_comma=False)
@add_slots
@dataclass(frozen=True)
class Lambda(BaseExpression):
# The parameters to the lambda
params: Parameters
# The body of the lambda
body: BaseExpression
# The colon separating the parameters from the body
colon: Colon = Colon(whitespace_after=SimpleWhitespace(" "))
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace
whitespace_after_lambda: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
# Validate parents
super(Lambda, self)._validate()
# Sum up all parameters
all_params = [
*self.params.params,
*self.params.default_params,
*self.params.kwonly_params,
]
if isinstance(self.params.star_arg, Param):
all_params.append(self.params.star_arg)
if self.params.star_kwarg is not None:
all_params.append(self.params.star_kwarg)
# Check for nonzero parameters because several checks care
# about this.
if len(all_params) > 0:
for param in all_params:
if param.annotation is not None:
raise CSTValidationError(
"Lambda params cannot have type annotations."
)
if (
isinstance(self.whitespace_after_lambda, BaseParenthesizableWhitespace)
and self.whitespace_after_lambda.empty
):
raise CSTValidationError(
"Must have at least one space after lambda when specifying params"
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Lambda":
return Lambda(
lpar=visit_sequence("lpar", self.lpar, visitor),
whitespace_after_lambda=visit_sentinel(
"whitespace_after_lambda", self.whitespace_after_lambda, visitor
),
params=visit_required("params", self.params, visitor),
colon=visit_required("colon", self.colon, visitor),
body=visit_required("body", self.body, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token("lambda")
whitespace_after_lambda = self.whitespace_after_lambda
if isinstance(whitespace_after_lambda, MaybeSentinel):
if not (
len(self.params.params) == 0
and len(self.params.default_params) == 0
and not isinstance(self.params.star_arg, Param)
and len(self.params.kwonly_params) == 0
and self.params.star_kwarg is None
):
# We have one or more params, provide a space
state.add_token(" ")
elif isinstance(whitespace_after_lambda, BaseParenthesizableWhitespace):
whitespace_after_lambda._codegen(state)
self.params._codegen(state)
self.colon._codegen(state)
self.body._codegen(state)
@add_slots
@dataclass(frozen=True)
class Arg(CSTNode):
"""
A single argument to a Call. It may be a * or a ** expansion, or it may be in
the form of "keyword=expression" for named arguments.
"""
# The argument expression itself
value: BaseExpression
# Optional keyword for the argument
keyword: Optional[Name] = None
# The equals sign used to denote assignment if there is a keyword.
equal: Union[AssignEqual, MaybeSentinel] = MaybeSentinel.DEFAULT
# Any trailing comma
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
# Optional star appearing before name for * and ** expansion
star: Literal["", "*", "**"] = ""
# Whitespace
whitespace_after_star: BaseParenthesizableWhitespace = SimpleWhitespace("")
whitespace_after_arg: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _validate(self) -> None:
if self.keyword is None and isinstance(self.equal, AssignEqual):
raise CSTValidationError(
"Must have a keyword when specifying an AssignEqual."
)
if self.star not in ("", "*", "**"):
raise CSTValidationError("Must specify either '', '*' or '**' for star.")
if self.star in ("*", "**") and self.keyword is not None:
raise CSTValidationError("Cannot specify a star and a keyword together.")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Arg":
return Arg(
star=self.star,
whitespace_after_star=visit_required(
"whitespace_after_star", self.whitespace_after_star, visitor
),
keyword=visit_optional("keyword", self.keyword, visitor),
equal=visit_sentinel("equal", self.equal, visitor),
value=visit_required("value", self.value, visitor),
comma=visit_sentinel("comma", self.comma, visitor),
whitespace_after_arg=visit_required(
"whitespace_after_arg", self.whitespace_after_arg, visitor
),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
state.add_token(self.star)
self.whitespace_after_star._codegen(state)
keyword = self.keyword
if keyword is not None:
keyword._codegen(state)
equal = self.equal
if equal is MaybeSentinel.DEFAULT and self.keyword is not None:
state.add_token(" = ")
elif isinstance(equal, AssignEqual):
equal._codegen(state)
self.value._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
self.whitespace_after_arg._codegen(state)
class _BaseExpressionWithArgs(BaseExpression, ABC):
"""
Arguments are complicated enough that we can't represent them easily
in typing. So, we have common validation functions here.
"""
# Sequence of arguments that will be passed to the functgion call
args: Sequence[Arg] = () # TODO This can also be a single Generator.
def _check_kwargs_or_keywords(
self, arg: Arg
) -> Optional[Callable[[Arg], Callable]]:
"""
Validates that we only have a mix of "keyword=arg" and "**arg" expansion.
"""
if arg.keyword is not None:
# Valid, keyword argument
return None
elif arg.star == "**":
# Valid, kwargs
return None
elif arg.star == "*":
# Invalid, cannot have "*" follow "**"
raise CSTValidationError(
"Cannot have iterable argument unpacking after keyword argument unpacking."
)
else:
# Invalid, cannot have positional argument follow **/keyword
raise CSTValidationError(
"Cannot have positional argument after keyword argument unpacking."
)
def _check_starred_or_keywords(
self, arg: Arg
) -> Optional[Callable[[Arg], Callable]]:
"""
Validates that we only have a mix of "*arg" expansion and "keyword=arg".
"""
if arg.keyword is not None:
# Valid, keyword argument
return None
elif arg.star == "**":
# Valid, but we now no longer allow "*" args
# pyre-fixme[7]: Expected `Optional[Callable[[Arg], Callable[...,
# Any]]]` but got `Callable[[Arg], Optional[Callable[[Arg], Callable[...,
# Any]]]]`.
return self._check_kwargs_or_keywords
elif arg.star == "*":
# Valid, iterable unpacking
return None
else:
# Invalid, cannot have positional argument follow **/keyword
raise CSTValidationError(
"Cannot have positional argument after keyword argument."
)
def _check_positional(self, arg: Arg) -> Optional[Callable[[Arg], Callable]]:
"""
Validates that we only have a mix of positional args and "*arg" expansion.
"""
if arg.keyword is not None:
# Valid, but this puts us into starred/keyword state
# pyre-fixme[7]: Expected `Optional[Callable[[Arg], Callable[...,
# Any]]]` but got `Callable[[Arg], Optional[Callable[[Arg], Callable[...,
# Any]]]]`.
return self._check_starred_or_keywords
elif arg.star == "**":
# Valid, but we skip states to kwargs/keywords
# pyre-fixme[7]: Expected `Optional[Callable[[Arg], Callable[...,
# Any]]]` but got `Callable[[Arg], Optional[Callable[[Arg], Callable[...,
# Any]]]]`.
return self._check_kwargs_or_keywords
elif arg.star == "*":
# Valid, iterator expansion
return None
else:
# Valid, allowed to have positional arguments here
return None
def _validate(self) -> None:
# Validate any super-class stuff, whatever it may be.
super()._validate()
# Now, validate the weird intermingling rules for arguments by running
# a small validator state machine. This works by passing each argument
# to a validator function which can either raise an exception if it
# detects an invalid sequence, return a new validator to be used for the
# next arg, or return None to use the same validator. We could enforce
# always returning ourselves instead of None but it ends up making the
# functions themselves less readable. In this way, the current validator
# function encodes the state we're in (positional state, iterable
# expansion state, or dictionary expansion state).
validator = self._check_positional
for arg in self.args:
# pyre-fixme[29]: `Union[Callable[[Arg], Callable[..., Any]],
# Callable[..., Any]]` is not a function.
validator = validator(arg) or validator
@add_slots
@dataclass(frozen=True)
class Call(_BaseExpressionWithArgs):
# The expression resulting in a callable that we are to call
func: Union[BaseAtom, Attribute, Subscript, "Call"]
# The arguments to pass to the resulting callable
args: Sequence[Arg] = () # TODO This can also be a single Generator.
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace nodes
whitespace_after_func: BaseParenthesizableWhitespace = SimpleWhitespace("")
whitespace_before_args: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _safe_to_use_with_word_operator(self, position: ExpressionPosition) -> bool:
"""
Calls have a close paren on the right side regardless of whether they're
parenthesized as a whole. As a result, they are safe to use directly against
an adjacent node to the right.
"""
if position == ExpressionPosition.LEFT:
return True
return super(Call, self)._safe_to_use_with_word_operator(position)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Call":
return Call(
lpar=visit_sequence("lpar", self.lpar, visitor),
func=visit_required("func", self.func, visitor),
whitespace_after_func=visit_required(
"whitespace_after_func", self.whitespace_after_func, visitor
),
whitespace_before_args=visit_required(
"whitespace_before_args", self.whitespace_before_args, visitor
),
args=visit_sequence("args", self.args, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
with state.record_syntactic_position(self):
self.func._codegen(state)
self.whitespace_after_func._codegen(state)
state.add_token("(")
self.whitespace_before_args._codegen(state)
lastarg = len(self.args) - 1
for i, arg in enumerate(self.args):
arg._codegen(state, default_comma=(i != lastarg))
state.add_token(")")
@add_slots
@dataclass(frozen=True)
class Await(BaseExpression):
# The actual expression we need to await on
expression: BaseExpression
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace nodes
whitespace_after_await: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
def _validate(self) -> None:
# Validate any super-class stuff, whatever it may be.
super(Await, self)._validate()
# Make sure we don't run identifiers together.
if self.whitespace_after_await.empty:
raise CSTValidationError("Must have at least one space after await")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Await":
return Await(
lpar=visit_sequence("lpar", self.lpar, visitor),
whitespace_after_await=visit_required(
"whitespace_after_await", self.whitespace_after_await, visitor
),
expression=visit_required("expression", self.expression, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token("await")
self.whitespace_after_await._codegen(state)
self.expression._codegen(state)
@add_slots
@dataclass(frozen=True)
class IfExp(BaseExpression):
"""
An if expression similar to "body if test else orelse".
"""
# The test to perform.
test: BaseExpression
# The expression to evaluate if the test is true.
body: BaseExpression
# The expression to evaluate if the test is false.
orelse: BaseExpression
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace nodes
whitespace_before_if: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
whitespace_after_if: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
whitespace_before_else: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
whitespace_after_else: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
def _validate(self) -> None:
# Paren validation and such
super(IfExp, self)._validate()
# Validate spacing rules
if (
self.whitespace_before_if.empty
and not self.body._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space before 'if' keyword."
)
if (
self.whitespace_after_if.empty
and not self.test._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError("Must have at least one space after 'if' keyword.")
if (
self.whitespace_before_else.empty
and not self.test._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space before 'else' keyword."
)
if (
self.whitespace_after_else.empty
and not self.orelse._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError(
"Must have at least one space after 'else' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "IfExp":
return IfExp(
lpar=visit_sequence("lpar", self.lpar, visitor),
body=visit_required("body", self.body, visitor),
whitespace_before_if=visit_required(
"whitespace_before_if", self.whitespace_before_if, visitor
),
whitespace_after_if=visit_required(
"whitespace_after_if", self.whitespace_after_if, visitor
),
test=visit_required("test", self.test, visitor),
whitespace_before_else=visit_required(
"whitespace_before_else", self.whitespace_before_else, visitor
),
whitespace_after_else=visit_required(
"whitespace_after_else", self.whitespace_after_else, visitor
),
orelse=visit_required("orelse", self.orelse, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.body._codegen(state)
self.whitespace_before_if._codegen(state)
state.add_token("if")
self.whitespace_after_if._codegen(state)
self.test._codegen(state)
self.whitespace_before_else._codegen(state)
state.add_token("else")
self.whitespace_after_else._codegen(state)
self.orelse._codegen(state)
@add_slots
@dataclass(frozen=True)
class From(CSTNode):
"""
A 'from x' stanza in a Yield or Raise.
"""
# Expression that we are yielding/raising from.
item: BaseExpression
whitespace_before_from: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
whitespace_after_from: BaseParenthesizableWhitespace = SimpleWhitespace(" ")
def _validate(self) -> None:
if (
isinstance(self.whitespace_after_from, BaseParenthesizableWhitespace)
and self.whitespace_after_from.empty
and not self.item._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError(
"Must have at least one space after 'from' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "From":
return From(
whitespace_before_from=visit_sentinel(
"whitespace_before_from", self.whitespace_before_from, visitor
),
whitespace_after_from=visit_required(
"whitespace_after_from", self.whitespace_after_from, visitor
),
item=visit_required("item", self.item, visitor),
)
def _codegen_impl(self, state: CodegenState, default_space: str = "") -> None:
whitespace_before_from = self.whitespace_before_from
if isinstance(whitespace_before_from, BaseParenthesizableWhitespace):
whitespace_before_from._codegen(state)
else:
state.add_token(default_space)
with state.record_syntactic_position(self):
state.add_token("from")
self.whitespace_after_from._codegen(state)
self.item._codegen(state)
@add_slots
@dataclass(frozen=True)
class Yield(BaseExpression):
"""
A yield expression similar to "yield x" or "yield from fun()"
"""
# The test to perform.
value: Optional[Union[BaseExpression, From]] = None
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
# Whitespace nodes
whitespace_after_yield: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
# Paren rules and such
super(Yield, self)._validate()
# Our own rules
if (
isinstance(self.whitespace_after_yield, BaseParenthesizableWhitespace)
and self.whitespace_after_yield.empty
):
if isinstance(self.value, From):
raise CSTValidationError(
"Must have at least one space after 'yield' keyword."
)
if isinstance(
self.value, BaseExpression
) and not self.value._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
):
raise CSTValidationError(
"Must have at least one space after 'yield' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Yield":
return Yield(
lpar=visit_sequence("lpar", self.lpar, visitor),
whitespace_after_yield=visit_sentinel(
"whitespace_after_yield", self.whitespace_after_yield, visitor
),
value=visit_optional("value", self.value, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
state.add_token("yield")
whitespace_after_yield = self.whitespace_after_yield
if isinstance(whitespace_after_yield, BaseParenthesizableWhitespace):
whitespace_after_yield._codegen(state)
else:
# Only need a space after yield if there is a value to yield.
if self.value is not None:
state.add_token(" ")
value = self.value
if isinstance(value, From):
value._codegen(state, default_space="")
elif value is not None:
value._codegen(state)
class BaseElement(CSTNode, ABC):
"""
An element of a literal list, tuple, or set. For elements of a literal dict, see
BaseMappingElement. (TODO)
"""
# pyre-fixme[13]: Attribute `value` is never initialized.
value: BaseExpression
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
@abstractmethod
def _codegen_impl(
self,
state: CodegenState,
default_comma: bool = False,
default_comma_whitespace: bool = False, # False for a single-item tuple
) -> None:
...
@add_slots
@dataclass(frozen=True)
class Element(BaseElement):
value: BaseExpression
# Any trailing comma
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Element":
return Element(
value=visit_required("value", self.value, visitor),
comma=visit_sentinel("comma", self.comma, visitor),
)
def _codegen_impl(
self,
state: CodegenState,
default_comma: bool = False,
default_comma_whitespace: bool = False,
) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
if default_comma_whitespace:
state.add_token(", ")
else:
state.add_token(",")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class StarredElement(BaseElement, _BaseParenthesizedNode):
value: BaseExpression
# Any trailing comma
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
# Parentheses around the leading asterisk and the value. Functionally equivalent to
# parentheses around the value, but in a different position.
lpar: Sequence[LeftParen] = ()
rpar: Sequence[RightParen] = ()
# Whitespace
whitespace_before_value: BaseParenthesizableWhitespace = SimpleWhitespace("")
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "StarredElement":
return StarredElement(
lpar=visit_sequence("lpar", self.lpar, visitor),
whitespace_before_value=visit_required(
"whitespace_before_value", self.whitespace_before_value, visitor
),
value=visit_required("value", self.value, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
comma=visit_sentinel("comma", self.comma, visitor),
)
def _codegen_impl(
self,
state: CodegenState,
default_comma: bool = False,
default_comma_whitespace: bool = False,
) -> None:
with self._parenthesize(state):
state.add_token("*")
self.whitespace_before_value._codegen(state)
self.value._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
if default_comma_whitespace:
state.add_token(", ")
else:
state.add_token(",")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class Tuple(BaseAtom, BaseAssignTargetExpression, BaseDelTargetExpression):
elements: Sequence[Union[Element, StarredElement]]
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = (LeftParen(),)
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = (RightParen(),)
def _safe_to_use_with_word_operator(self, position: ExpressionPosition) -> bool:
if super(Tuple, self)._safe_to_use_with_word_operator(position):
# if we have parenthesis, we're safe.
return True
# elements[-1] and elements[0] must exist past this point, because
# we're not parenthesized, meaning we must have at least one element.
elements = self.elements
if position == ExpressionPosition.LEFT:
last_element = elements[-1]
return (
isinstance(last_element.comma, Comma)
or (
isinstance(last_element, StarredElement)
and len(last_element.rpar) > 0
)
or last_element.value._safe_to_use_with_word_operator(position)
)
else: # ExpressionPosition.RIGHT
first_element = elements[0]
# starred elements are always safe because they begin with ( or *
return isinstance(
first_element, StarredElement
) or first_element.value._safe_to_use_with_word_operator(position)
def _validate(self) -> None:
# Paren validation and such
super(Tuple, self)._validate()
if len(self.elements) == 0:
if len(self.lpar) == 0: # assumes len(lpar) == len(rpar), via superclass
raise CSTValidationError(
"A zero-length tuple must be wrapped in parentheses."
)
# Invalid commas aren't possible, because MaybeSentinel will ensure that there
# is a comma where required.
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "Tuple":
return Tuple(
lpar=visit_sequence("lpar", self.lpar, visitor),
elements=visit_sequence("elements", self.elements, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
elements = self.elements
if len(elements) == 1:
elements[0]._codegen(
state, default_comma=True, default_comma_whitespace=False
)
else:
for idx, el in enumerate(elements):
el._codegen(
state,
default_comma=(idx < len(elements) - 1),
default_comma_whitespace=True,
)
@add_slots
@dataclass(frozen=True)
class List(BaseAtom, BaseAssignTargetExpression, BaseDelTargetExpression):
elements: Sequence[Union[Element, StarredElement]]
# Open bracket surrounding the list
lbracket: LeftSquareBracket = LeftSquareBracket()
# Close bracket surrounding the list
rbracket: RightSquareBracket = RightSquareBracket()
# Sequence of open parenthesis for precedence dictation.
lpar: Sequence[LeftParen] = ()
# Sequence of close parenthesis for precedence dictation.
rpar: Sequence[RightParen] = ()
def _safe_to_use_with_word_operator(self, position: ExpressionPosition) -> bool:
return True
def _visit_and_replace_children(self, visitor: CSTVisitor) -> "List":
return List(
lpar=visit_sequence("lpar", self.lpar, visitor),
lbracket=visit_required("lbracket", self.lbracket, visitor),
elements=visit_sequence("elements", self.elements, visitor),
rbracket=visit_required("rbracket", self.rbracket, visitor),
rpar=visit_sequence("rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.lbracket._codegen(state)
elements = self.elements
for idx, el in enumerate(elements):
el._codegen(
state,
default_comma=(idx < len(elements) - 1),
default_comma_whitespace=True,
)
self.rbracket._codegen(state)