src/Phan/Analysis/AssignOperatorFlagVisitor.php
<?php
declare(strict_types=1);
namespace Phan\Analysis;
use ast;
use ast\Node;
use Phan\AST\UnionTypeVisitor;
use Phan\AST\Visitor\Element;
use Phan\AST\Visitor\FlagVisitorImplementation;
use Phan\CodeBase;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\FloatType;
use Phan\Language\Type\IntType;
use Phan\Language\Type\StringType;
use Phan\Language\UnionType;
/**
* This visitor returns a Context with the updated changes caused by an assignment operation (e.g. changes to Variables, Variable types)
*
* TODO: Improve analysis of bitwise operations, warn if non-int is provided and consistently return int if it's guaranteed
*
* TODO: Make the type inferences of this stay in sync with AssignOperatorAnalysisVisitor.
*/
class AssignOperatorFlagVisitor extends FlagVisitorImplementation
{
/**
* @var CodeBase The code base within which we're operating
*/
private $code_base;
/**
* @var Context The context in which we are determining the union type of the result of an assignment operator
*/
private $context;
/**
* Create a new AssignOperatorFlagVisitor
*/
public function __construct(
CodeBase $code_base,
Context $context
) {
$this->code_base = $code_base;
$this->context = $context;
}
/**
* @param Node $node
* A node to visit
* @return UnionType
*/
public function __invoke(Node $node)
{
// NOTE: Some operations currently don't exist in any php version, such as `$x ||= 2;`, `$x xor= 2;`
return Element::acceptBinaryFlagVisitor($node, $this);
}
/**
* Default visitor for node kinds that do not have
* an overriding method
*
* @param Node $node
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visit(Node $node): UnionType
{
// TODO: For some types (e.g. xor, bitwise or), set the type of the variable?
// Or should that be done in PreOrderAnalysisVisitor?
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
if ($left->isExclusivelyArray()
|| $right->isExclusivelyArray()
) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeArrayOperator,
$node->lineno ?? 0,
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags],
$left,
$right
);
return UnionType::empty();
} elseif ($left->hasNonNullIntType()
&& $right->hasNonNullIntType()
) {
return IntType::instance(false)->asPHPDocUnionType();
} elseif ($left->hasType(FloatType::instance(false))
&& $right->hasType(FloatType::instance(false))
) {
return FloatType::instance(false)->asPHPDocUnionType();
}
static $int_or_float;
return $int_or_float ?? ($int_or_float = UnionType::fromFullyQualifiedPHPDocString('int|float'));
}
public function visitBinaryCoalesce(Node $node): UnionType
{
$var_node = $node->children['var'];
$new_node = new ast\Node(ast\AST_BINARY_OP, $node->lineno, [
'left' => $var_node,
'right' => $node->children['expr'],
], ast\flags\BINARY_COALESCE);
return (new BinaryOperatorFlagVisitor(
$this->code_base,
$this->context,
true
))->visitBinaryCoalesce($new_node);
}
/**
* @return UnionType for the `&` operator
*/
public function visitBinaryBitwiseAnd(Node $node): UnionType
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
if ($left->hasNonNullIntType()
&& $right->hasNonNullIntType()
) {
return IntType::instance(false)->asPHPDocUnionType();
} elseif ($left->hasNonNullStringType() &&
$right->hasNonNullStringType()) {
// $x = 'a'; $x &= 'c';
return StringType::instance(false)->asPHPDocUnionType();
}
return IntType::instance(false)->asPHPDocUnionType();
}
/**
* @return UnionType for the `|` operator
*/
public function visitBinaryBitwiseOr(Node $node): UnionType
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
if ($left->hasNonNullIntType()
&& $right->hasNonNullIntType()
) {
return IntType::instance(false)->asPHPDocUnionType();
} elseif ($left->hasNonNullStringType() &&
$right->hasNonNullStringType()) {
// $x = 'a'; $x |= 'c';
return StringType::instance(false)->asPHPDocUnionType();
}
return IntType::instance(false)->asPHPDocUnionType();
}
/**
* Analyze the bitwise xor operator.
*
* NOTE: Code can bitwise xor strings byte by byte in PHP
*
* @return UnionType for the `^` operator
*/
public function visitBinaryBitwiseXor(Node $node): UnionType
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
// TODO: check for other invalid types
if ($left->isExclusivelyArray()
|| $right->isExclusivelyArray()
) {
// TODO: Move these checks into AssignOperatorAnalysisVisitor
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeArrayOperator,
$node->lineno ?? 0,
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags],
$left,
$right
);
return UnionType::empty();
} elseif ($left->hasNonNullIntType()
&& $right->hasNonNullIntType()
) {
return IntType::instance(false)->asPHPDocUnionType();
} elseif ($left->hasNonNullStringType()
&& $right->hasNonNullStringType()
) {
return StringType::instance(false)->asPHPDocUnionType();
}
return IntType::instance(false)->asPHPDocUnionType();
}
/**
* @param Node $node @phan-unused-param
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryConcat(Node $node): UnionType
{
return StringType::instance(false)->asRealUnionType();
}
/**
* @param Node $node
* A node to check types on
*
* @return UnionType
* The resulting type(s) of the binary operation
*/
public function visitBinaryAdd(Node $node): UnionType
{
static $int_or_float_or_array;
static $probably_int_type;
static $probably_array_type;
static $probably_float_type;
static $probably_int_or_float_type;
static $unknown_type;
if ($int_or_float_or_array === null) {
$int_or_float_or_array = [IntType::instance(false), FloatType::instance(false), ArrayType::instance(false)];
$probably_float_type = UnionType::of([FloatType::instance(false)], $int_or_float_or_array);
$probably_int_or_float_type = UnionType::of([IntType::instance(false), FloatType::instance(false)], $int_or_float_or_array);
$probably_int_type = UnionType::of([IntType::instance(false)], $int_or_float_or_array);
$probably_array_type = UnionType::of([ArrayType::instance(false)], $int_or_float_or_array);
$unknown_type = UnionType::of([], $int_or_float_or_array);
}
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
// fast-track common cases
if ($left->isNonNullIntType()
&& $right->isNonNullIntType()
) {
return $probably_int_type;
}
// If both left and right are arrays, then this is array
// concatenation.
if ($left->isGenericArray() && $right->isGenericArray()) {
if ($left->isEqualTo($right)) {
return $left;
}
return $probably_array_type;
}
// TODO: isNonNullNumberType
if (($left->isNonNullIntType()
|| $left->isType(FloatType::instance(false)))
&& ($right->isNonNullIntType()
|| $right->isType(FloatType::instance(false)))
) {
return $probably_float_type;
}
$left_is_array = (
!$left->genericArrayElementTypes()->isEmpty()
&& $left->nonArrayTypes()->isEmpty()
) || $left->isType(ArrayType::instance(false));
$right_is_array = (
!$right->genericArrayElementTypes()->isEmpty()
&& $right->nonArrayTypes()->isEmpty()
) || $right->isType(ArrayType::instance(false));
if ($left_is_array
&& !$right->canCastToUnionType(
ArrayType::instance(false)->asPHPDocUnionType()
)
) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeInvalidRightOperand,
$node->lineno ?? 0
);
return $unknown_type;
} elseif ($right_is_array
&& !$left->canCastToUnionType(ArrayType::instance(false)->asPHPDocUnionType())
) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeInvalidLeftOperand,
$node->lineno ?? 0
);
return $unknown_type;
} elseif ($left_is_array || $right_is_array) {
// If it is a '+' and we know one side is an array
// and the other is unknown, assume array
return $probably_array_type;
}
return $probably_int_or_float_type;
}
/**
* @unused-param $node
* @override
*/
public function visitBinaryDiv(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return FloatType::instance(false)->asRealUnionType();
}
/** @override */
public function visitBinaryMul(Node $node): UnionType
{
// both sides are analyzed for issues in AssignOperatorAnalysisVisitor
return $this->optimisticAnalyzeNumericOp($node);
}
/** @override */
public function visitBinarySub(Node $node): UnionType
{
return $this->optimisticAnalyzeNumericOp($node);
}
private function optimisticAnalyzeNumericOp(Node $node): UnionType
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['var']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr']
);
if ($left->hasNonNullIntType()
&& $right->hasNonNullIntType()
) {
static $int_type = null;
return $int_type ?? ($int_type = UnionType::of([IntType::instance(false)], [FloatType::instance(false)]));
}
// analyzed in AssignOperatorAnalysisVisitor
return FloatType::instance(false)->asRealUnionType();
}
/**
* @unused-param $node
* @override
*/
public function visitBinaryMod(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return IntType::instance(false)->asRealUnionType();
}
/**
* @unused-param $node
* @override
*/
public function visitBinaryPow(Node $node): UnionType
{
// analyzed in AssignOperatorAnalysisVisitor
return FloatType::instance(false)->asRealUnionType();
}
/**
* @unused-param $node
* @override
*/
public function visitBinaryShiftLeft(Node $node): UnionType
{
return IntType::instance(false)->asRealUnionType();
}
/**
* @unused-param $node
* @override
*/
public function visitBinaryShiftRight(Node $node): UnionType
{
return IntType::instance(false)->asRealUnionType();
}
}