Module refinery.lib.scripts.php.precedence
PHP operator precedence and parenthesization rules.
The precedence levels and associativities are taken from the Zend grammar's operator declarations
(Zend/zend_language_parser.y, the %left/%right/%nonassoc/%precedence block). As in the
JavaScript module, the synthesizer does not depend on PhpParenExpression nodes for correctness: it
consults needs_parens() to decide, from operator precedence, whether a child expression must be
wrapped in its parent context.
Note the PHP 8 specifics encoded here: . (concatenation) sits below the shift and additive
operators (it moved in PHP 8), ?? and ** are right-associative, the word operators and/or/xor
bind below assignment, and casts bind below ** so that (int) $a ** $b groups as
(int) ($a ** $b).
Expand source code Browse git
"""
PHP operator precedence and parenthesization rules.
The precedence levels and associativities are taken from the Zend grammar's operator declarations
(`Zend/zend_language_parser.y`, the `%left`/`%right`/`%nonassoc`/`%precedence` block). As in the
JavaScript module, the synthesizer does not depend on `PhpParenExpression` nodes for correctness: it
consults `needs_parens` to decide, from operator precedence, whether a child expression must be
wrapped in its parent context.
Note the PHP 8 specifics encoded here: `.` (concatenation) sits *below* the shift and additive
operators (it moved in PHP 8), `??` and `**` are right-associative, the word operators `and`/`or`/`xor`
bind *below* assignment, and casts bind *below* `**` so that `(int) $a ** $b` groups as
`(int) ($a ** $b)`.
"""
from __future__ import annotations
from refinery.lib.scripts import Node
from refinery.lib.scripts.php.model import (
PhpArrayDimFetch,
PhpArrowFunction,
PhpAssignment,
PhpBinaryExpression,
PhpCastExpression,
PhpClassConstFetch,
PhpClone,
PhpErrorSuppress,
PhpFunctionCall,
PhpInclude,
PhpInstanceof,
PhpMethodCall,
PhpNew,
PhpNewAnonymous,
PhpParenExpression,
PhpPrint,
PhpPropertyFetch,
PhpStaticCall,
PhpStaticPropertyFetch,
PhpTernary,
PhpThrowExpression,
PhpUnaryExpression,
PhpUpdateExpression,
PhpYield,
PhpYieldFrom,
)
_BINARY_PRECEDENCE = {
'or' : 3, # noqa
'xor' : 4, # noqa
'and' : 5, # noqa
'??' : 11, # noqa
'||' : 12, # noqa
'&&' : 13, # noqa
'|' : 14, # noqa
'^' : 15, # noqa
'&' : 16, # noqa
'==' : 17, # noqa
'!=' : 17, # noqa
'<>' : 17, # noqa
'===' : 17, # noqa
'!==' : 17, # noqa
'<=>' : 17, # noqa
'<' : 18, # noqa
'<=' : 18, # noqa
'>' : 18, # noqa
'>=' : 18, # noqa
'|>' : 19, # noqa
'.' : 20, # noqa
'<<' : 21, # noqa
'>>' : 21, # noqa
'+' : 22, # noqa
'-' : 22, # noqa
'*' : 23, # noqa
'/' : 23, # noqa
'%' : 23, # noqa
'**' : 27, # noqa
}
_RIGHT_ASSOCIATIVE = frozenset({'??', '**'})
_THROW_PRECEDENCE = 1
_ARROW_FUNCTION_PRECEDENCE = 1
_INCLUDE_PRECEDENCE = 2
_PRINT_PRECEDENCE = 6
_YIELD_PRECEDENCE = 7
_ASSIGN_PRECEDENCE = 9
_TERNARY_PRECEDENCE = 10
_LOGICAL_NOT_PRECEDENCE = 24
_INSTANCEOF_PRECEDENCE = 25
_UNARY_PRECEDENCE = 26
_CLONE_PRECEDENCE = 28
_PRIMARY_PRECEDENCE = 100
def _expression_precedence(node: Node) -> int:
"""
Return the operator precedence of *node* on the PHP scale used by `parens_required`. Primary
expressions (variables, literals, names, calls, member/array access, and the like) return a
value high enough that they never need parenthesisation.
"""
if isinstance(node, PhpBinaryExpression):
return _BINARY_PRECEDENCE.get(node.operator, 0)
if isinstance(node, PhpInstanceof):
return _INSTANCEOF_PRECEDENCE
if isinstance(node, PhpUnaryExpression):
if node.operator == '!':
return _LOGICAL_NOT_PRECEDENCE
return _UNARY_PRECEDENCE
if isinstance(node, (PhpCastExpression, PhpErrorSuppress)):
return _UNARY_PRECEDENCE
if isinstance(node, PhpClone):
return _CLONE_PRECEDENCE
if isinstance(node, PhpAssignment):
return _ASSIGN_PRECEDENCE
if isinstance(node, PhpTernary):
return _TERNARY_PRECEDENCE
if isinstance(node, PhpThrowExpression):
return _THROW_PRECEDENCE
if isinstance(node, PhpArrowFunction):
return _ARROW_FUNCTION_PRECEDENCE
if isinstance(node, (PhpYield, PhpYieldFrom)):
return _YIELD_PRECEDENCE
if isinstance(node, PhpPrint):
return _PRINT_PRECEDENCE
if isinstance(node, PhpInclude):
return _INCLUDE_PRECEDENCE
return _PRIMARY_PRECEDENCE
def _leading_sign(node: Node) -> str | None:
"""
Return `+` or `-` if *node* is synthesized starting with that sign character, otherwise `None`.
Such a node cannot directly follow a prefix `+`/`-` operator without an intervening paren, since
the synthesizer emits no separator and the two would merge into a `++`/`--` token.
"""
if isinstance(node, PhpUnaryExpression) and node.operator in ('+', '-'):
return node.operator
if isinstance(node, PhpUpdateExpression) and node.prefix and node.operator in ('++', '--'):
return node.operator[0]
return None
def _is_primary_receiver(node: Node) -> bool:
"""
Return whether *node* may sit as the receiver/callee/class in a member access, call, array
access, or `new` context without parentheses. Any operator expression in such a position would
rebind and must keep its parentheses.
"""
return _expression_precedence(node) >= _PRIMARY_PRECEDENCE
def parens_required(inner: Node, parent: Node | None, paren_node: Node) -> bool:
"""
Return whether *paren_node* (positioned within *parent*, wrapping *inner*) must be parenthesized
to preserve the program's meaning. *paren_node* is the node actually occupying the slot in
*parent*; *inner* is the expression whose precedence is examined. When the two are identical the
question is simply whether *inner* needs parentheses in *parent* — see `needs_parens`.
"""
if parent is None:
return False
inner_p = _expression_precedence(inner)
if isinstance(parent, PhpBinaryExpression):
outer_p = _BINARY_PRECEDENCE.get(parent.operator, 0)
if inner_p > outer_p:
return False
if inner_p < outer_p:
return True
if parent.operator in _RIGHT_ASSOCIATIVE:
return parent.left is paren_node
return parent.right is paren_node
if isinstance(parent, PhpInstanceof):
if parent.operand is paren_node:
return inner_p < _INSTANCEOF_PRECEDENCE
return False
if isinstance(parent, PhpUnaryExpression):
if parent.operator in ('+', '-') and _leading_sign(inner) == parent.operator:
return True
outer_p = _LOGICAL_NOT_PRECEDENCE if parent.operator == '!' else _UNARY_PRECEDENCE
return inner_p < outer_p
if isinstance(parent, (PhpCastExpression, PhpErrorSuppress)):
return inner_p < _UNARY_PRECEDENCE
if isinstance(parent, PhpClone):
return inner_p < _CLONE_PRECEDENCE
if isinstance(parent, PhpTernary):
return inner_p <= _TERNARY_PRECEDENCE
if isinstance(parent, PhpAssignment):
if parent.target is paren_node:
return False
return inner_p < _ASSIGN_PRECEDENCE
if isinstance(parent, (PhpPropertyFetch, PhpMethodCall)):
if parent.receiver is paren_node:
return not _is_primary_receiver(inner)
return False
if isinstance(parent, PhpArrayDimFetch):
if parent.receiver is paren_node:
return not _is_primary_receiver(inner)
return False
if isinstance(parent, PhpFunctionCall):
if parent.callee is paren_node:
return not _is_primary_receiver(inner)
return False
if isinstance(parent, (PhpStaticCall, PhpStaticPropertyFetch, PhpClassConstFetch)):
if parent.class_name is paren_node:
return not _is_primary_receiver(inner)
return False
if isinstance(parent, PhpNew):
if parent.class_name is paren_node:
return not _is_primary_receiver(inner)
return False
if isinstance(parent, PhpNewAnonymous):
return not _is_primary_receiver(inner)
return False
def needs_parens(child: Node, parent: Node | None) -> bool:
"""
Return whether *child*, which currently occupies a slot in *parent*, must be wrapped in
parentheses to preserve precedence. Used by the synthesizer at print time so that correctness
does not depend on `PhpParenExpression` nodes being present in the tree.
"""
if isinstance(child, PhpParenExpression):
return False
return parens_required(child, parent, child)
def statement_needs_parens(expr: Node) -> bool:
"""
Return whether an expression statement consisting of *expr* must be parenthesized because of a
leading-token hazard. PHP delimits statements explicitly and has no object-literal or block
ambiguity at the start of an expression statement, so no such hazard exists; the function is
provided for parity with the JavaScript module and always returns `False`.
"""
del expr
return False
Functions
def parens_required(inner, parent, paren_node)-
Return whether paren_node (positioned within parent, wrapping inner) must be parenthesized to preserve the program's meaning. paren_node is the node actually occupying the slot in parent; inner is the expression whose precedence is examined. When the two are identical the question is simply whether inner needs parentheses in parent — see
needs_parens().Expand source code Browse git
def parens_required(inner: Node, parent: Node | None, paren_node: Node) -> bool: """ Return whether *paren_node* (positioned within *parent*, wrapping *inner*) must be parenthesized to preserve the program's meaning. *paren_node* is the node actually occupying the slot in *parent*; *inner* is the expression whose precedence is examined. When the two are identical the question is simply whether *inner* needs parentheses in *parent* — see `needs_parens`. """ if parent is None: return False inner_p = _expression_precedence(inner) if isinstance(parent, PhpBinaryExpression): outer_p = _BINARY_PRECEDENCE.get(parent.operator, 0) if inner_p > outer_p: return False if inner_p < outer_p: return True if parent.operator in _RIGHT_ASSOCIATIVE: return parent.left is paren_node return parent.right is paren_node if isinstance(parent, PhpInstanceof): if parent.operand is paren_node: return inner_p < _INSTANCEOF_PRECEDENCE return False if isinstance(parent, PhpUnaryExpression): if parent.operator in ('+', '-') and _leading_sign(inner) == parent.operator: return True outer_p = _LOGICAL_NOT_PRECEDENCE if parent.operator == '!' else _UNARY_PRECEDENCE return inner_p < outer_p if isinstance(parent, (PhpCastExpression, PhpErrorSuppress)): return inner_p < _UNARY_PRECEDENCE if isinstance(parent, PhpClone): return inner_p < _CLONE_PRECEDENCE if isinstance(parent, PhpTernary): return inner_p <= _TERNARY_PRECEDENCE if isinstance(parent, PhpAssignment): if parent.target is paren_node: return False return inner_p < _ASSIGN_PRECEDENCE if isinstance(parent, (PhpPropertyFetch, PhpMethodCall)): if parent.receiver is paren_node: return not _is_primary_receiver(inner) return False if isinstance(parent, PhpArrayDimFetch): if parent.receiver is paren_node: return not _is_primary_receiver(inner) return False if isinstance(parent, PhpFunctionCall): if parent.callee is paren_node: return not _is_primary_receiver(inner) return False if isinstance(parent, (PhpStaticCall, PhpStaticPropertyFetch, PhpClassConstFetch)): if parent.class_name is paren_node: return not _is_primary_receiver(inner) return False if isinstance(parent, PhpNew): if parent.class_name is paren_node: return not _is_primary_receiver(inner) return False if isinstance(parent, PhpNewAnonymous): return not _is_primary_receiver(inner) return False def needs_parens(child, parent)-
Return whether child, which currently occupies a slot in parent, must be wrapped in parentheses to preserve precedence. Used by the synthesizer at print time so that correctness does not depend on
PhpParenExpressionnodes being present in the tree.Expand source code Browse git
def needs_parens(child: Node, parent: Node | None) -> bool: """ Return whether *child*, which currently occupies a slot in *parent*, must be wrapped in parentheses to preserve precedence. Used by the synthesizer at print time so that correctness does not depend on `PhpParenExpression` nodes being present in the tree. """ if isinstance(child, PhpParenExpression): return False return parens_required(child, parent, child) def statement_needs_parens(expr)-
Return whether an expression statement consisting of expr must be parenthesized because of a leading-token hazard. PHP delimits statements explicitly and has no object-literal or block ambiguity at the start of an expression statement, so no such hazard exists; the function is provided for parity with the JavaScript module and always returns
False.Expand source code Browse git
def statement_needs_parens(expr: Node) -> bool: """ Return whether an expression statement consisting of *expr* must be parenthesized because of a leading-token hazard. PHP delimits statements explicitly and has no object-literal or block ambiguity at the start of an expression statement, so no such hazard exists; the function is provided for parity with the JavaScript module and always returns `False`. """ del expr return False