src/Phan/Analysis/PostOrderAnalysisVisitor.php
<?php
declare(strict_types=1);
namespace Phan\Analysis;
use AssertionError;
use ast;
use ast\flags;
use ast\Node;
use Closure;
use Exception;
use Phan\AST\AnalysisVisitor;
use Phan\AST\ASTReverter;
use Phan\AST\ContextNode;
use Phan\AST\PhanAnnotationAdder;
use Phan\AST\ScopeImpactCheckingVisitor;
use Phan\AST\UnionTypeVisitor;
use Phan\BlockAnalysisVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Exception\CodeBaseException;
use Phan\Exception\EmptyFQSENException;
use Phan\Exception\FQSENException;
use Phan\Exception\IssueException;
use Phan\Exception\NodeException;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\IssueFixSuggester;
use Phan\Language\Context;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Language\Element\Parameter;
use Phan\Language\Element\PassByReferenceVariable;
use Phan\Language\Element\Property;
use Phan\Language\Element\Variable;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedGlobalConstantName;
use Phan\Language\Type;
use Phan\Language\Type\ArrayType;
use Phan\Language\Type\FalseType;
use Phan\Language\Type\GenericArrayType;
use Phan\Language\Type\IntType;
use Phan\Language\Type\LiteralFloatType;
use Phan\Language\Type\LiteralStringType;
use Phan\Language\Type\MixedType;
use Phan\Language\Type\NonEmptyMixedType;
use Phan\Language\Type\NonNullMixedType;
use Phan\Language\Type\NullType;
use Phan\Language\Type\ObjectType;
use Phan\Language\Type\StringType;
use Phan\Language\Type\VoidType;
use Phan\Language\UnionType;
use function end;
use function implode;
use function sprintf;
/**
* PostOrderAnalysisVisitor is where we do the post-order part of the analysis
* during Phan's analysis phase.
*
* This is called in post-order by BlockAnalysisVisitor
* (i.e. this is called after visiting all children of the current node)
*
* @phan-file-suppress PhanPartialTypeMismatchArgument
*/
class PostOrderAnalysisVisitor extends AnalysisVisitor
{
/**
* @var list<Node> a list of parent nodes of the currently analyzed node,
* within the current global or function-like scope
*/
private $parent_node_list;
/**
* @param CodeBase $code_base
* A code base needs to be passed in because we require
* it to be initialized before any classes or files are
* loaded.
*
* @param Context $context
* The context of the parser at the node for which we'd
* like to determine a type
*
* @param list<Node> $parent_node_list
* The parent node list of the node being analyzed
*/
public function __construct(
CodeBase $code_base,
Context $context,
array $parent_node_list
) {
parent::__construct($code_base, $context);
$this->parent_node_list = $parent_node_list;
}
/**
* Default visitor for node kinds that do not have
* an overriding method
*
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visit(Node $node): Context
{
// Many nodes don't change the context and we
// don't need to read them.
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitAssign(Node $node): Context
{
// Get the type of the right side of the
// assignment
$right_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr'],
true
);
$var_node = $node->children['var'];
if (!($var_node instanceof Node)) {
// Give up, this should be impossible except with the fallback
$this->emitIssue(
Issue::InvalidNode,
$node->lineno,
"Expected left side of assignment to be a variable"
);
return $this->context;
}
if ($right_type->isVoidType()) {
$this->emitIssue(
Issue::TypeVoidAssignment,
$node->lineno
);
}
// Handle the assignment based on the type of the
// right side of the equation and the kind of item
// on the left.
// (AssignmentVisitor converts possibly undefined types to nullable)
$context = (new AssignmentVisitor(
$this->code_base,
$this->context,
$node,
$right_type
))->__invoke($var_node);
$expr_node = $node->children['expr'];
if ($expr_node instanceof Node
&& $expr_node->kind === ast\AST_CLOSURE
) {
$method = (new ContextNode(
$this->code_base,
$this->context->withLineNumberStart(
$expr_node->lineno
),
$expr_node
))->getClosure();
$method->addReference($this->context);
}
return $context;
}
/**
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitAssignRef(Node $node): Context
{
return $this->visitAssign($node);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
* @override
*/
public function visitAssignOp(Node $node): Context
{
return (new AssignOperatorAnalysisVisitor($this->code_base, $this->context))->__invoke($node);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitUnset(Node $node): Context
{
$context = $this->context;
// Get the type of the thing being unset
$var_node = $node->children['var'];
if (!($var_node instanceof Node)) {
return $context;
}
$kind = $var_node->kind;
if ($kind === ast\AST_VAR) {
$var_name = $var_node->children['name'];
if (\is_string($var_name)) {
// TODO: Make this work in branches
$context->unsetScopeVariable($var_name);
}
// I think DollarDollarPlugin already warns, so don't warn here.
} elseif ($kind === ast\AST_DIM) {
$this->analyzeUnsetDim($var_node);
} elseif ($kind === ast\AST_PROP) {
return $this->analyzeUnsetProp($var_node);
}
return $context;
}
/**
* @param Node $node a node of type AST_DIM in unset()
* @see UnionTypeVisitor::resolveArrayShapeElementTypes()
* @see UnionTypeVisitor::visitDim()
*/
private function analyzeUnsetDim(Node $node): void
{
$expr_node = $node->children['expr'];
if (!($expr_node instanceof Node)) {
// php -l would warn
return;
}
// For now, just handle a single level of dimensions for unset($x['field']);
if ($expr_node->kind === ast\AST_VAR) {
$var_name = $expr_node->children['name'];
if (!\is_string($var_name)) {
return;
}
$context = $this->context;
$scope = $context->getScope();
if (!$scope->hasVariableWithName($var_name)) {
// TODO: Warn about potentially pointless unset in function scopes?
return;
}
// TODO: Could warn about invalid offsets for isset
$variable = $scope->getVariableByName($var_name);
$union_type = $variable->getUnionType();
if ($union_type->isEmpty()) {
return;
}
$resolved_union_type = $union_type->withStaticResolvedInContext($this->context);
if (!$resolved_union_type->asExpandedTypes($this->code_base)->hasArrayLike() && !$resolved_union_type->hasMixedType()) {
$this->emitIssue(
Issue::TypeArrayUnsetSuspicious,
$node->lineno,
ASTReverter::toShortString($expr_node),
(string)$resolved_union_type
);
}
$dim_node = $node->children['dim'];
$dim_value = $dim_node instanceof Node ? (new ContextNode($this->code_base, $this->context, $dim_node))->getEquivalentPHPScalarValue() : $dim_node;
// unset($x[$i]) should convert a list<T> or non-empty-list<T> to an array<Y>
$union_type = $union_type->withAssociativeArrays(true)->asMappedUnionType(static function (Type $type): Type {
if ($type instanceof NonEmptyMixedType) {
// convert non-empty-mixed to non-null-mixed because `unset($x[$i])` could have removed the last element of an array,
// but that would still not be null.
return $type->isNullableLabeled() ? MixedType::instance(true) : NonNullMixedType::instance(false);
}
return $type;
});
$variable = clone($variable);
$context->addScopeVariable($variable);
$variable->setUnionType($union_type);
/*
if (!is_scalar($dim_value) || (!is_numeric($dim_value) || $dim_value >= 0)) {
foreach ($union_type->getTypeSet() as $type) {
if ($type instanceof ListType) {
$union_type = $union_type->withoutType($type)->withType(
GenericArrayType::fromElementType($type->genericArrayElementType(), false, $type->getKeyType())
);
$variable = clone($variable);
$context->addScopeVariable($variable);
$variable->setUnionType($union_type);
}
}
}
*/
if (!$union_type->hasTopLevelArrayShapeTypeInstances()) {
return;
}
// TODO: detect and warn about null
if (!\is_scalar($dim_value)) {
return;
}
$variable->setUnionType($union_type->withoutArrayShapeField($dim_value));
}
}
/**
* @param Node $node a node of type AST_PROP in unset()
* @see UnionTypeVisitor::resolveArrayShapeElementTypes()
* @see UnionTypeVisitor::visitDim()
*/
private function analyzeUnsetProp(Node $node): Context
{
$expr_node = $node->children['expr'];
$context = $this->context;
if (!($expr_node instanceof Node)) {
// php -l would warn
return $context;
}
$prop_name = $node->children['prop'];
if (!\is_string($prop_name)) {
$prop_name = (new ContextNode($this->code_base, $this->context, $prop_name))->getEquivalentPHPScalarValue();
if (!\is_string($prop_name)) {
return $context;
}
}
if ($expr_node->kind === \ast\AST_VAR && $expr_node->children['name'] === 'this' && $context === $this->context) {
$context = $context->withThisPropertySetToTypeByName($prop_name, NullType::instance(false)->asPHPDocUnionType()->withIsDefinitelyUndefined());
}
$union_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr_node)->withStaticResolvedInContext($this->context);
$type_fqsens = $union_type->objectTypesWithKnownFQSENs();
foreach ($type_fqsens->getTypeSet() as $type) {
$fqsen = FullyQualifiedClassName::fromType($type);
if (!$this->code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$class = $this->code_base->getClassByFQSEN($fqsen);
if ($class->hasPropertyWithName($this->code_base, $prop_name)) {
// NOTE: We deliberately emit this issue whether or not the access is to a public or private variable,
// because unsetting a private variable at runtime is also a (failed) attempt to unset a declared property.
$prop = $class->getPropertyByName($this->code_base, $prop_name);
if ($prop->isFromPHPDoc()) {
// TODO: Warn if __get is defined but __unset isn't defined?
continue;
}
if ($prop->isDynamicProperty()) {
continue;
}
$this->emitIssue(
Issue::TypeObjectUnsetDeclaredProperty,
$node->lineno,
(string)$type,
$prop_name,
$prop->getFileRef()->getFile(),
$prop->getFileRef()->getLineNumberStart()
);
}
}
return $context;
}
/**
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitIfElem(Node $node): Context
{
return $this->context;
}
/**
* @param Node $node @phan-unused-param
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitWhile(Node $node): Context
{
return $this->context;
}
/**
* @param Node $node @phan-unused-param
* A node of kind ast\AST_SWITCH to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*
* @suppress PhanUndeclaredProperty
*/
public function visitSwitch(Node $node): Context
{
if (isset($node->phan_loop_contexts)) {
// Combine contexts from continue/break statements within this do-while loop
$context = (new ContextMergeVisitor($this->context, \array_merge([$this->context], $node->phan_loop_contexts)))->combineChildContextList();
unset($node->phan_loop_contexts);
return $context;
}
return $this->context;
}
/**
* @param Node $node @phan-unused-param
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitSwitchCase(Node $node): Context
{
return $this->context;
}
/**
* @param Node $node @phan-unused-param
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitExprList(Node $node): Context
{
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitEncapsList(Node $node): Context
{
$this->analyzeNoOp($node, Issue::NoopEncapsulatedStringLiteral);
foreach ($node->children as $child_node) {
// Confirm that variables exists
if (!($child_node instanceof Node)) {
continue;
}
$this->checkExpressionInDynamicString($child_node);
}
return $this->context;
}
private function checkExpressionInDynamicString(Node $expr_node): void
{
$code_base = $this->code_base;
$context = $this->context;
$type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$expr_node,
true
);
if (!$type->hasPrintableScalar()) {
if ($type->isType(ArrayType::instance(false))
|| $type->isType(ArrayType::instance(true))
|| $type->isGenericArray()
) {
$this->emitIssue(
Issue::TypeConversionFromArray,
$expr_node->lineno,
'string'
);
return;
}
// Check for __toString(), stringable variables/expressions in encapsulated strings work whether or not strict_types is set
try {
foreach ($type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, "__toString", true)) {
return;
}
}
} catch (CodeBaseException | RecursionDepthException $_) {
// Swallow "Cannot find class" or recursion exceptions, go on to emit issue
}
$this->emitIssue(
Issue::TypeSuspiciousStringExpression,
$expr_node->lineno,
(string)$type,
ASTReverter::toShortString($expr_node)
);
}
}
/**
* Check if a given variable is undeclared.
* @param Node $node Node with kind AST_VAR
*/
private function checkForUndeclaredVariable(Node $node): void
{
$variable_name = $node->children['name'];
// Ignore $$var type things
if (!\is_string($variable_name)) {
return;
}
// Don't worry about non-existent undeclared variables
// in the global scope if configured to do so
if (Config::getValue('ignore_undeclared_variables_in_global_scope')
&& $this->context->isInGlobalScope()
) {
return;
}
if (!$this->context->getScope()->hasVariableWithName($variable_name)
&& !Variable::isHardcodedVariableInScopeWithName($variable_name, $this->context->isInGlobalScope())
) {
$this->emitIssueWithSuggestion(
Variable::chooseIssueForUndeclaredVariable($this->context, $variable_name),
$node->lineno,
[$variable_name],
IssueFixSuggester::suggestVariableTypoFix($this->code_base, $this->context, $variable_name)
);
}
}
/**
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitDoWhile(Node $node): Context
{
return $this->context;
}
/**
* Visit a node with kind `ast\AST_GLOBAL`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitGlobal(Node $node): Context
{
$variable_name = $node->children['var']->children['name'] ?? null;
if (!\is_string($variable_name)) {
// Shouldn't happen?
return $this->context;
}
$variable = new Variable(
$this->context->withLineNumberStart($node->lineno),
$variable_name,
UnionType::empty(),
0
);
$optional_global_variable_type = Variable::getUnionTypeOfHardcodedGlobalVariableWithName($variable_name);
if ($optional_global_variable_type) {
$variable->setUnionType($optional_global_variable_type);
} else {
$scope = $this->context->getScope();
if ($scope->hasGlobalVariableWithName($variable_name)) {
// TODO: Support @global, add a clone to the method context?
$actual_global_variable = clone($scope->getGlobalVariableByName($variable_name));
$actual_global_variable->setUnionType($actual_global_variable->getUnionType()->eraseRealTypeSetRecursively());
$this->context->addScopeVariable($actual_global_variable);
return $this->context;
}
}
// Note that we're not creating a new scope, just
// adding variables to the existing scope
$this->context->addScopeVariable($variable);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitStatic(Node $node): Context
{
$variable = Variable::fromNodeInContext(
$node->children['var'],
$this->context,
$this->code_base,
false
);
// If the element has a default, set its type
// on the variable
if (isset($node->children['default'])) {
$default_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['default']
);
} else {
$default_type = NullType::instance(false)->asRealUnionType();
}
// NOTE: Phan can't be sure that the type the static type starts with is the same as what it has later. Avoid false positive PhanRedundantCondition.
$variable->setUnionType($default_type->eraseRealTypeSetRecursively());
// TODO: Probably not true in a loop?
// TODO: Expand this to assigning to variables? (would need to make references invalidate that, and skip this in the global scope)
$variable->enablePhanFlagBits(\Phan\Language\Element\Flags::IS_CONSTANT_DEFINITION);
// Note that we're not creating a new scope, just
// adding variables to the existing scope
$this->context->addScopeVariable($variable);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitEcho(Node $node): Context
{
return $this->visitPrint($node);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitPrint(Node $node): Context
{
$code_base = $this->code_base;
$context = $this->context;
$expr_node = $node->children['expr'];
$type = UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$expr_node,
true
);
if (!$type->hasPrintableScalar()) {
if ($type->isType(ArrayType::instance(false))
|| $type->isType(ArrayType::instance(true))
|| $type->isGenericArray()
) {
$this->emitIssue(
Issue::TypeConversionFromArray,
$expr_node->lineno ?? $node->lineno,
'string'
);
return $context;
}
if (!$context->isStrictTypes()) {
try {
foreach ($type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->asClassList($code_base, $context) as $clazz) {
if ($clazz->hasMethodWithName($code_base, "__toString", true)) {
return $context;
}
}
} catch (CodeBaseException $_) {
// Swallow "Cannot find class", go on to emit issue
}
}
$this->emitIssue(
Issue::TypeSuspiciousEcho,
$expr_node->lineno ?? $node->lineno,
ASTReverter::toShortString($expr_node),
(string)$type
);
}
return $context;
}
/**
* These types are either types which create variables,
* or types which will be checked in other parts of Phan
*/
private const SKIP_VAR_CHECK_TYPES = [
ast\AST_ARG_LIST => true, // may be a reference
ast\AST_ARRAY_ELEM => true, // [$x, $y] = expr() is an AST_ARRAY_ELEM. visitArray() checks the right-hand side.
ast\AST_ASSIGN_OP => true, // checked in visitAssignOp
ast\AST_ASSIGN_REF => true, // Creates by reference?
ast\AST_ASSIGN => true, // checked in visitAssign
ast\AST_DIM => true, // should be checked elsewhere, as part of check for array access to non-array/string
ast\AST_EMPTY => true, // TODO: Enable this in the future?
ast\AST_GLOBAL => true, // global $var;
ast\AST_ISSET => true, // TODO: Enable this in the future?
ast\AST_PARAM_LIST => true, // this creates the variable
ast\AST_STATIC => true, // static $var;
ast\AST_STMT_LIST => true, // ;$var; (Implicitly creates the variable. Already checked to emit PhanNoopVariable)
ast\AST_USE_ELEM => true, // may be a reference, checked elsewhere
];
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitVar(Node $node): Context
{
$this->analyzeNoOp($node, Issue::NoopVariable);
$parent_node = \end($this->parent_node_list);
if ($parent_node instanceof Node && !($node->flags & PhanAnnotationAdder::FLAG_IGNORE_UNDEF)) {
$parent_kind = $parent_node->kind;
if (!\array_key_exists($parent_kind, self::SKIP_VAR_CHECK_TYPES)) {
$this->checkForUndeclaredVariable($node);
}
}
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitArray(Node $node): Context
{
$this->analyzeNoOp($node, Issue::NoopArray);
return $this->context;
}
/** @internal */
public const NAME_FOR_BINARY_OP = [
flags\BINARY_BOOL_AND => '&&',
flags\BINARY_BOOL_OR => '||',
flags\BINARY_BOOL_XOR => 'xor',
flags\BINARY_BITWISE_OR => '|',
flags\BINARY_BITWISE_AND => '&',
flags\BINARY_BITWISE_XOR => '^',
flags\BINARY_CONCAT => '.',
flags\BINARY_ADD => '+',
flags\BINARY_SUB => '-',
flags\BINARY_MUL => '*',
flags\BINARY_DIV => '/',
flags\BINARY_MOD => '%',
flags\BINARY_POW => '**',
flags\BINARY_SHIFT_LEFT => '<<',
flags\BINARY_SHIFT_RIGHT => '>>',
flags\BINARY_IS_IDENTICAL => '===',
flags\BINARY_IS_NOT_IDENTICAL => '!==',
flags\BINARY_IS_EQUAL => '==',
flags\BINARY_IS_NOT_EQUAL => '!=',
flags\BINARY_IS_SMALLER => '<',
flags\BINARY_IS_SMALLER_OR_EQUAL => '<=',
flags\BINARY_IS_GREATER => '>',
flags\BINARY_IS_GREATER_OR_EQUAL => '>=',
flags\BINARY_SPACESHIP => '<=>',
flags\BINARY_COALESCE => '??',
];
/**
* @param Node $node
* A node of type AST_BINARY_OP to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitBinaryOp(Node $node): Context
{
$flags = $node->flags;
if ($this->isInNoOpPosition($node)) {
if (\in_array($flags, [flags\BINARY_BOOL_AND, flags\BINARY_BOOL_OR, flags\BINARY_COALESCE], true)) {
// @phan-suppress-next-line PhanAccessMethodInternal
if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['right'])) {
$this->emitIssue(
Issue::NoopBinaryOperator,
$node->lineno,
self::NAME_FOR_BINARY_OP[$flags] ?? ''
);
}
} else {
$this->emitIssue(
Issue::NoopBinaryOperator,
$node->lineno,
self::NAME_FOR_BINARY_OP[$flags] ?? ''
);
}
}
switch ($flags) {
case flags\BINARY_CONCAT:
$this->analyzeBinaryConcat($node);
break;
case flags\BINARY_DIV:
case flags\BINARY_POW:
case flags\BINARY_MOD:
$this->analyzeBinaryNumericOp($node);
break;
case flags\BINARY_SHIFT_LEFT:
case flags\BINARY_SHIFT_RIGHT:
$this->analyzeBinaryShift($node);
break;
case flags\BINARY_BITWISE_OR:
case flags\BINARY_BITWISE_AND:
case flags\BINARY_BITWISE_XOR:
$this->analyzeBinaryBitwiseOp($node);
break;
}
return $this->context;
}
private function analyzeBinaryShift(Node $node): void
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['left']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['right']
);
$this->warnAboutInvalidUnionType(
$node,
static function (Type $type): bool {
if ($type->isNullableLabeled()) {
return false;
}
if ($type instanceof IntType || $type instanceof MixedType) {
return true;
}
if ($type instanceof LiteralFloatType) {
return $type->isValidBitwiseOperand();
}
return false;
},
$left,
$right,
Issue::TypeInvalidLeftOperandOfIntegerOp,
Issue::TypeInvalidRightOperandOfIntegerOp
);
}
private function analyzeBinaryBitwiseOp(Node $node): void
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['left']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['right']
);
$this->warnAboutInvalidUnionType(
$node,
static function (Type $type): bool {
if ($type->isNullableLabeled()) {
return false;
}
if ($type instanceof IntType || $type instanceof StringType || $type instanceof MixedType) {
return true;
}
if ($type instanceof LiteralFloatType) {
return $type->isValidBitwiseOperand();
}
return false;
},
$left,
$right,
Issue::TypeInvalidLeftOperandOfBitwiseOp,
Issue::TypeInvalidRightOperandOfBitwiseOp
);
}
/** @internal used by AssignOperatorAnalysisVisitor */
public const ISSUE_TYPES_RIGHT_SIDE_ZERO = [
flags\BINARY_POW => Issue::PowerOfZero,
flags\BINARY_DIV => Issue::DivisionByZero,
flags\BINARY_MOD => Issue::ModuloByZero,
];
private function analyzeBinaryNumericOp(Node $node): void
{
$left = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['left']
);
$right = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['right']
);
if (!$right->isEmpty() && !$right->containsTruthy()) {
$this->emitIssue(
self::ISSUE_TYPES_RIGHT_SIDE_ZERO[$node->flags],
$node->children['right']->lineno ?? $node->lineno,
ASTReverter::toShortString($node),
$right
);
}
$this->warnAboutInvalidUnionType(
$node,
static function (Type $type): bool {
return $type->isValidNumericOperand();
},
$left,
$right,
Issue::TypeInvalidLeftOperandOfNumericOp,
Issue::TypeInvalidRightOperandOfNumericOp
);
}
/**
* @param Node $node with type AST_BINARY_OP
* @param Closure(Type):bool $is_valid_type
*/
private function warnAboutInvalidUnionType(
Node $node,
Closure $is_valid_type,
UnionType $left,
UnionType $right,
string $left_issue_type,
string $right_issue_type
): void {
if (!$left->isEmpty()) {
if (!$left->hasTypeMatchingCallback($is_valid_type)) {
$this->emitIssue(
$left_issue_type,
$node->children['left']->lineno ?? $node->lineno,
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags],
$left
);
}
}
if (!$right->isEmpty()) {
if (!$right->hasTypeMatchingCallback($is_valid_type)) {
$this->emitIssue(
$right_issue_type,
$node->children['right']->lineno ?? $node->lineno,
PostOrderAnalysisVisitor::NAME_FOR_BINARY_OP[$node->flags],
$right
);
}
}
}
private function analyzeBinaryConcat(Node $node): void
{
$left = $node->children['left'];
if ($left instanceof Node) {
$this->checkExpressionInDynamicString($left);
}
$right = $node->children['right'];
if ($right instanceof Node) {
$this->checkExpressionInDynamicString($right);
}
}
public const NAME_FOR_UNARY_OP = [
flags\UNARY_BOOL_NOT => '!',
flags\UNARY_BITWISE_NOT => '~',
flags\UNARY_SILENCE => '@',
flags\UNARY_PLUS => '+',
flags\UNARY_MINUS => '-',
];
/**
* @param Node $node
* A node of type AST_EMPTY to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitEmpty(Node $node): Context
{
if ($this->isInNoOpPosition($node)) {
$this->emitIssue(
Issue::NoopEmpty,
$node->lineno,
ASTReverter::toShortString($node->children['expr'])
);
}
return $this->context;
}
/**
* @internal
* Maps the flags of nodes with kind AST_CAST to their types
*/
public const AST_CAST_FLAGS_LOOKUP = [
flags\TYPE_NULL => 'unset',
flags\TYPE_BOOL => 'bool',
flags\TYPE_LONG => 'int',
flags\TYPE_DOUBLE => 'float',
flags\TYPE_STRING => 'string',
flags\TYPE_ARRAY => 'array',
flags\TYPE_OBJECT => 'object',
// These aren't casts, but they are used in various places
flags\TYPE_CALLABLE => 'callable',
flags\TYPE_VOID => 'void',
flags\TYPE_ITERABLE => 'iterable',
flags\TYPE_FALSE => 'false',
flags\TYPE_STATIC => 'static',
];
/**
* @suppress PhanUselessBinaryAddRight this replaces 'unset' with 'null'
*/
public const AST_TYPE_FLAGS_LOOKUP = [
ast\flags\TYPE_NULL => 'null',
] + self::AST_CAST_FLAGS_LOOKUP;
/**
* @param Node $node
* A node of type ast\AST_CAST to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitCast(Node $node): Context
{
if ($this->isInNoOpPosition($node)) {
$this->emitIssue(
Issue::NoopCast,
$node->lineno,
self::AST_CAST_FLAGS_LOOKUP[$node->flags] ?? 'unknown',
ASTReverter::toShortString($node->children['expr'])
);
}
if ($node->flags === flags\TYPE_NULL) {
$this->emitIssue(
Issue::CompatibleUnsetCast,
$node->lineno,
ASTReverter::toShortString($node)
);
}
return $this->context;
}
/**
* @param Node $node
* A node of kind ast\AST_ISSET to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitIsset(Node $node): Context
{
if ($this->isInNoOpPosition($node)) {
$this->emitIssue(
Issue::NoopIsset,
$node->lineno,
ASTReverter::toShortString($node->children['var'])
);
}
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitUnaryOp(Node $node): Context
{
if ($node->flags === flags\UNARY_SILENCE) {
$expr = $node->children['expr'];
if ($expr instanceof Node) {
if ($expr->kind === ast\AST_UNARY_OP && $expr->flags === flags\UNARY_SILENCE) {
$this->emitIssue(
Issue::NoopRepeatedSilenceOperator,
$node->lineno,
ASTReverter::toShortString($node)
);
}
} else {
// TODO: Other node kinds
$this->emitIssue(
Issue::NoopUnaryOperator,
$node->lineno,
self::NAME_FOR_UNARY_OP[$node->flags] ?? ''
);
}
} else {
if ($this->isInNoOpPosition($node)) {
$this->emitIssue(
Issue::NoopUnaryOperator,
$node->lineno,
self::NAME_FOR_UNARY_OP[$node->flags] ?? ''
);
}
}
return $this->context;
}
/**
* @override
*/
public function visitPreInc(Node $node): Context
{
return $this->analyzeIncOrDec($node);
}
/**
* @override
*/
public function visitPostInc(Node $node): Context
{
return $this->analyzeIncOrDec($node);
}
/**
* @override
*/
public function visitPreDec(Node $node): Context
{
return $this->analyzeIncOrDec($node);
}
/**
* @override
*/
public function visitPostDec(Node $node): Context
{
return $this->analyzeIncOrDec($node);
}
public const NAME_FOR_INC_OR_DEC_KIND = [
ast\AST_PRE_INC => '++(expr)',
ast\AST_PRE_DEC => '--(expr)',
ast\AST_POST_INC => '(expr)++',
ast\AST_POST_DEC => '(expr)--',
];
private function analyzeIncOrDec(Node $node): Context
{
$var = $node->children['var'];
$old_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $var);
if (!$old_type->canCastToUnionType(UnionType::fromFullyQualifiedPHPDocString('int|string|float'))) {
$this->emitIssue(
Issue::TypeInvalidUnaryOperandIncOrDec,
$node->lineno,
self::NAME_FOR_INC_OR_DEC_KIND[$node->kind],
$old_type
);
}
// The left can be a non-Node for an invalid AST
$kind = $var->kind ?? null;
if ($kind === \ast\AST_VAR) {
$new_type = $old_type->getTypeAfterIncOrDec();
if ($old_type === $new_type) {
return $this->context;
}
if (!$this->context->isInLoop()) {
try {
$value = $old_type->asSingleScalarValueOrNull();
if (\is_numeric($value)) {
if ($node->kind === ast\AST_POST_DEC || $node->kind === ast\AST_PRE_DEC) {
@--$value;
} else {
@++$value;
}
// TODO: Compute the real type set.
$new_type = Type::fromObject($value)->asPHPDocUnionType();
}
} catch (\Throwable $_) {
// ignore
}
}
try {
$variable = (new ContextNode($this->code_base, $this->context, $var))->getVariableStrict();
} catch (IssueException | NodeException $_) {
return $this->context;
}
$variable = clone($variable);
$variable->setUnionType($new_type);
$this->context->addScopeVariable($variable);
return $this->context;
}
// Treat expr++ like expr -= -1 and expr-- like expr -= 1.
// Use `-` to avoid false positives about array operations.
// (This isn't 100% accurate for invalid types)
$new_node = new Node(
ast\AST_ASSIGN_OP,
ast\flags\BINARY_SUB,
[
'var' => $var,
'expr' => ($node->kind === ast\AST_POST_DEC || $node->kind === ast\AST_PRE_DEC) ? 1 : -1,
],
$node->lineno
);
return (new AssignOperatorAnalysisVisitor($this->code_base, $this->context))->visitBinarySub($new_node);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitConst(Node $node): Context
{
$context = $this->context;
try {
// Based on UnionTypeVisitor::visitConst
$constant = (new ContextNode(
$this->code_base,
$context,
$node
))->getConst();
// Mark that this constant has been referenced from
// this context
$constant->addReference($context);
} catch (IssueException $exception) {
// We need to do this in order to check keys and (after the first 5) values in AST arrays.
// Other parts of the AST may also not be covered.
// (This issue may be a duplicate)
Issue::maybeEmitInstance(
$this->code_base,
$context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// Swallow any other types of exceptions. We'll log the errors
// elsewhere.
}
// Check to make sure we're doing something with the
// constant
$this->analyzeNoOp($node, Issue::NoopConstant);
return $context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitClassConst(Node $node): Context
{
try {
$constant = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getClassConst();
// Mark that this class constant has been referenced
// from this context
$constant->addReference($this->context);
} catch (IssueException $exception) {
// We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types.
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// Swallow any other types of exceptions. We'll log the errors
// elsewhere.
}
// Check to make sure we're doing something with the
// class constant
$this->analyzeNoOp($node, Issue::NoopConstant);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitClassConstDecl(Node $node): Context
{
$class = $this->context->getClassInScope($this->code_base);
foreach ($node->children as $child_node) {
if (!$child_node instanceof Node) {
throw new AssertionError('expected class const element to be a Node');
}
$name = $child_node->children['name'];
if (!\is_string($name)) {
throw new AssertionError('expected class const name to be a string');
}
try {
$const_decl = $class->getConstantByNameInContext($this->code_base, $name, $this->context);
$const_decl->getUnionType();
} catch (IssueException $exception) {
// We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types.
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// Swallow any other types of exceptions. We'll log the errors
// elsewhere.
}
}
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitConstDecl(Node $node): Context
{
foreach ($node->children as $child_node) {
if (!$child_node instanceof Node) {
throw new AssertionError('expected const element to be a Node');
}
$name = $child_node->children['name'];
if (!\is_string($name)) {
throw new AssertionError('expected const name to be a string');
}
try {
$fqsen = FullyQualifiedGlobalConstantName::fromStringInContext(
$name,
$this->context
);
$const_decl = $this->code_base->getGlobalConstantByFQSEN($fqsen);
$const_decl->getUnionType();
} catch (IssueException $exception) {
// We need to do this in order to check keys and (after the first 5) values in AST arrays, possibly other types.
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// Swallow any other types of exceptions. We'll log the errors
// elsewhere.
}
}
return $this->context;
}
/**
* @param Node $node
* A node of kind `ast\AST_CLASS_NAME` to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitClassName(Node $node): Context
{
try {
foreach ((new ContextNode(
$this->code_base,
$this->context,
$node->children['class']
))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME) as $class) {
$class->addReference($this->context);
}
} catch (CodeBaseException $exception) {
$exception_fqsen = $exception->getFQSEN();
$this->emitIssueWithSuggestion(
Issue::UndeclaredClassReference,
$node->lineno,
[(string)$exception_fqsen],
IssueFixSuggester::suggestSimilarClassForGenericFQSEN($this->code_base, $this->context, $exception_fqsen)
);
} catch (IssueException $exception) {
Issue::maybeEmitInstance($this->code_base, $this->context, $exception->getIssueInstance());
}
// Check to make sure we're doing something with the
// ::class class constant
$this->analyzeNoOp($node, Issue::NoopConstant);
return $this->context;
}
/**
* @param Node $node
* A node of kind ast\AST_CLOSURE or ast\AST_ARROW_FUNC to analyze
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitClosure(Node $node): Context
{
$func = $this->context->getFunctionLikeInScope($this->code_base);
$return_type = $func->getUnionType();
if (!$return_type->isEmpty()
&& !$func->hasReturn()
&& !self::declOnlyThrows($node)
&& !$return_type->hasType(VoidType::instance(false))
&& !$return_type->hasType(NullType::instance(false))
) {
$this->warnTypeMissingReturn($func, $node);
}
$uses = $node->children['uses'] ?? null;
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($uses->polyfill_has_trailing_comma) && Config::get_closest_minimum_target_php_version_id() < 80000) {
$this->emitIssue(
Issue::CompatibleTrailingCommaParameterList,
end($uses->children)->lineno ?? $uses->lineno,
ASTReverter::toShortString($node)
);
}
$this->analyzeNoOp($node, Issue::NoopClosure);
$this->checkForFunctionInterfaceIssues($node, $func);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitArrowFunc(Node $node): Context
{
if (Config::get_closest_minimum_target_php_version_id() < 70400) {
$this->emitIssue(
Issue::CompatibleArrowFunction,
$node->lineno,
ASTReverter::toShortString($node)
);
}
return $this->visitClosure($node);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitReturn(Node $node): Context
{
$context = $this->context;
// Make sure we're actually returning from a method.
if (!$context->isInFunctionLikeScope()) {
return $context;
}
$code_base = $this->code_base;
// Check real return types instead of phpdoc return types in traits for #800
// TODO: Why did Phan originally not analyze return types of traits at all in 4c6956c05222e093b29393ceaa389ffb91041bdc
$is_trait = false;
if ($context->isInClassScope()) {
$clazz = $context->getClassInScope($code_base);
$is_trait = $clazz->isTrait();
}
// Get the method/function/closure we're in
$method = $context->getFunctionLikeInScope($code_base);
// Mark the method as returning something (even if void)
if (null !== $node->children['expr']) {
$method->setHasReturn(true);
}
if ($method->returnsRef()) {
$this->analyzeReturnsReference($method, $node);
}
if ($method->hasYield()) { // Function that is syntactically a Generator.
$this->analyzeReturnInGenerator($method, $node);
// TODO: Compare against TReturn of Generator<TKey,TValue,TSend,TReturn>
return $context; // Analysis was completed in PreOrderAnalysisVisitor
}
// Figure out what we intend to return
// (For traits, lower the false positive rate by comparing against the real return type instead of the phpdoc type (#800))
$method_return_type = $is_trait ? $method->getRealReturnType()->withAddedClassForResolvedSelf($method->getContext()) : $method->getUnionType();
$expr = $node->children['expr'];
// Check for failing to return a value, or returning a value in a void method.
if ($expr !== null) {
if ($method_return_type->hasRealTypeSet() && $method_return_type->asRealUnionType()->isVoidType()) {
$this->emitIssue(
Issue::SyntaxReturnValueInVoid,
$expr->lineno ?? $node->lineno,
'void',
$method->getNameForIssue(),
'return;',
'return ' . ASTReverter::toShortString($expr) . ';'
);
return $context;
}
} else {
// `function test() : ?string { return; }` is a fatal error. (We already checked for generators)
if ($method_return_type->hasRealTypeSet() && !$method_return_type->asRealUnionType()->isVoidType()) {
$this->emitIssue(
Issue::SyntaxReturnExpectedValue,
$node->lineno,
$method->getNameForIssue(),
$method_return_type,
'return null',
'return'
);
return $context;
}
}
// This leaves functions which aren't syntactically generators.
// Figure out what is actually being returned
// TODO: Properly check return values of array shapes
foreach ($this->getReturnTypes($context, $expr, $node->lineno) as $lineno => [$expression_type, $inner_node]) {
// If there is no declared type, see if we can deduce
// what it should be based on the return type
if ($method_return_type->isEmpty()
|| $method->isReturnTypeUndefined()
) {
if (!$is_trait) {
$method->setIsReturnTypeUndefined(true);
// Set the inferred type of the method based
// on what we're returning
$method->setUnionType($method->getUnionType()->withUnionType($expression_type));
}
// No point in comparing this type to the
// type we just set
continue;
}
// Check if the return type is compatible with the declared return type.
$is_mismatch = false;
if (!$method->isReturnTypeUndefined()) {
$resolved_expression_type = $expression_type->withStaticResolvedInContext($context);
// We allow base classes to cast to subclasses, and subclasses to cast to base classes,
// but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree
if (!$this->checkCanCastToReturnType($resolved_expression_type, $method_return_type)) {
$this->emitTypeMismatchReturnIssue($resolved_expression_type, $method, $method_return_type, $lineno, $inner_node);
$is_mismatch = true;
} elseif (Config::get_strict_return_checking() && $resolved_expression_type->typeCount() > 1) {
$is_mismatch = self::analyzeReturnStrict($code_base, $method, $resolved_expression_type, $method_return_type, $lineno, $inner_node);
}
}
// For functions that aren't syntactically Generators,
// update the set/existence of return values.
if ($method->isReturnTypeModifiable() && !$is_mismatch) {
// Add the new type to the set of values returned by the
// method
$method->setUnionType($method->getUnionType()->withUnionType($expression_type));
}
}
return $context;
}
/**
* @param Node $node a node of kind ast\AST_RETURN
*/
private function analyzeReturnsReference(FunctionInterface $method, Node $node): void
{
$expr = $node->children['expr'];
if ((!$expr instanceof Node) || !\in_array($expr->kind, ArgumentType::REFERENCE_NODE_KINDS, true)) {
$is_possible_reference = ArgumentType::isExpressionReturningReference($this->code_base, $this->context, $expr);
if (!$is_possible_reference) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeNonVarReturnByRef,
$expr->lineno ?? $node->lineno,
$method->getRepresentationForIssue()
);
}
}
}
/**
* Emits Issue::TypeMismatchReturnNullable or TypeMismatchReturn, unless suppressed
* @param Node|string|int|float|null $inner_node
*/
private function emitTypeMismatchReturnIssue(UnionType $expression_type, FunctionInterface $method, UnionType $method_return_type, int $lineno, $inner_node): void
{
if ($this->shouldSuppressIssue(Issue::TypeMismatchReturnReal, $lineno)) {
// Suppressing TypeMismatchReturnReal also suppresses less severe return type mismatches
return;
}
if (!$expression_type->isNull() && $this->checkCanCastToReturnTypeIfWasNonNullInstead($expression_type, $method_return_type)) {
if ($this->shouldSuppressIssue(Issue::TypeMismatchReturn, $lineno)) {
// Suppressing TypeMismatchReturn also suppresses TypeMismatchReturnNullable
return;
}
$issue_type = Issue::TypeMismatchReturnNullable;
} else {
$issue_type = Issue::TypeMismatchReturn;
// TODO: Don't warn for callable <-> string
if ($method_return_type->hasRealTypeSet()) {
// Always emit a real type warning about returning a value in a void method
$real_method_return_type = $method_return_type->getRealUnionType();
$real_expression_type = $expression_type->getRealUnionType();
if ($real_method_return_type->isVoidType() ||
($expression_type->hasRealTypeSet() && !$real_expression_type->canCastToDeclaredType($this->code_base, $this->context, $real_method_return_type))) {
$this->emitIssue(
Issue::TypeMismatchReturnReal,
$lineno,
self::returnExpressionToShortString($inner_node),
(string)$expression_type,
self::toDetailsForRealTypeMismatch($expression_type),
$method->getNameForIssue(),
(string)$method_return_type,
self::toDetailsForRealTypeMismatch($method_return_type)
);
return;
}
}
}
if ($this->context->hasSuppressIssue($this->code_base, Issue::TypeMismatchArgumentProbablyReal)) {
// Suppressing ProbablyReal also suppresses the less severe version.
return;
}
if ($issue_type === Issue::TypeMismatchReturn) {
if ($expression_type->hasRealTypeSet() &&
!$expression_type->getRealUnionType()->canCastToDeclaredType($this->code_base, $this->context, $method_return_type)) {
// The argument's real type is completely incompatible with the documented phpdoc type.
//
// Either the phpdoc type is wrong or the argument is likely wrong.
$this->emitIssue(
Issue::TypeMismatchReturnProbablyReal,
$lineno,
self::returnExpressionToShortString($inner_node),
$expression_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($expression_type),
$method->getNameForIssue(),
$method_return_type,
PostOrderAnalysisVisitor::toDetailsForRealTypeMismatch($method_return_type)
);
return;
}
}
$this->emitIssue(
$issue_type,
$lineno,
self::returnExpressionToShortString($inner_node),
(string)$expression_type,
$method->getNameForIssue(),
(string)$method_return_type
);
}
/**
* Converts the type to a description of the real type (if different from phpdoc type) for Phan's issue messages
* @internal
*/
public static function toDetailsForRealTypeMismatch(UnionType $type): string
{
$real_type = $type->getRealUnionType();
if ($real_type->isEqualTo($type)) {
return '';
}
if ($real_type->isEmpty()) {
return ' (no real type)';
}
return " (real type $real_type)";
}
private function analyzeReturnInGenerator(
FunctionInterface $method,
Node $node
): void {
$method_generator_type = $method->getReturnTypeAsGeneratorTemplateType();
$type_list = $method_generator_type->getTemplateParameterTypeList();
// Generator<TKey,TValue,TSend,TReturn>
if (\count($type_list) !== 4) {
return;
}
$expected_return_type = $type_list[3];
if ($expected_return_type->isEmpty()) {
return;
}
$context = $this->context;
$code_base = $this->code_base;
foreach ($this->getReturnTypes($context, $node->children['expr'], $node->lineno) as $lineno => [$expression_type, $inner_node]) {
$expression_type = $expression_type->withStaticResolvedInContext($context);
// We allow base classes to cast to subclasses, and subclasses to cast to base classes,
// but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree
if (!self::checkCanCastToReturnType($expression_type, $expected_return_type)) {
$this->emitTypeMismatchReturnIssue($expression_type, $method, $expected_return_type, $lineno, $inner_node);
} elseif (Config::get_strict_return_checking() && $expression_type->typeCount() > 1) {
self::analyzeReturnStrict($code_base, $method, $expression_type, $expected_return_type, $lineno, $inner_node);
}
}
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitYield(Node $node): Context
{
$context = $this->context;
// Make sure we're actually returning from a method.
if (!$context->isInFunctionLikeScope()) {
return $context;
}
// Get the method/function/closure we're in
$method = $context->getFunctionLikeInScope($this->code_base);
// Figure out what we intend to return
$method_generator_type = $method->getReturnTypeAsGeneratorTemplateType();
$type_list = $method_generator_type->getTemplateParameterTypeList();
if (\count($type_list) === 0) {
return $context;
}
return $this->compareYieldAgainstDeclaredType($node, $method, $context, $type_list);
}
/**
* @param list<UnionType> $template_type_list
*/
private function compareYieldAgainstDeclaredType(Node $node, FunctionInterface $method, Context $context, array $template_type_list): Context
{
$code_base = $this->code_base;
$type_list_count = \count($template_type_list);
$yield_value_node = $node->children['value'];
if ($yield_value_node === null) {
$yield_value_type = VoidType::instance(false)->asRealUnionType();
} else {
$yield_value_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $yield_value_node);
}
$expected_value_type = $template_type_list[\min(1, $type_list_count - 1)];
try {
if (!$yield_value_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_value_type->withStaticResolvedInContext($context))) {
$this->emitIssue(
Issue::TypeMismatchGeneratorYieldValue,
$node->lineno,
ASTReverter::toShortString($yield_value_node),
(string)$yield_value_type,
$method->getNameForIssue(),
(string)$expected_value_type,
'\Generator<' . implode(',', $template_type_list) . '>'
);
}
} catch (RecursionDepthException $_) {
}
if ($type_list_count > 1) {
$yield_key_node = $node->children['key'];
if ($yield_key_node === null) {
$yield_key_type = VoidType::instance(false)->asRealUnionType();
} else {
$yield_key_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $yield_key_node);
}
// TODO: finalize syntax to indicate the absence of a key or value (e.g. use void instead?)
$expected_key_type = $template_type_list[0];
if (!$yield_key_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_key_type->withStaticResolvedInContext($context))) {
$this->emitIssue(
Issue::TypeMismatchGeneratorYieldKey,
$node->lineno,
ASTReverter::toShortString($yield_key_node),
(string)$yield_key_type,
$method->getNameForIssue(),
(string)$expected_key_type,
'\Generator<' . implode(',', $template_type_list) . '>'
);
}
}
return $context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitYieldFrom(Node $node): Context
{
$context = $this->context;
// Make sure we're actually returning from a method.
if (!$context->isInFunctionLikeScope()) {
return $context;
}
// Get the method/function/closure we're in
$method = $context->getFunctionLikeInScope($this->code_base);
$code_base = $this->code_base;
$yield_from_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $node->children['expr']);
if ($yield_from_type->isEmpty()) {
return $context;
}
$yield_from_expanded_type = $yield_from_type->withStaticResolvedInContext($this->context)->asExpandedTypes($code_base);
if (!$yield_from_expanded_type->hasIterable() && !$yield_from_expanded_type->hasTraversable()) {
$this->emitIssue(
Issue::TypeInvalidYieldFrom,
$node->lineno,
ASTReverter::toShortString($node),
(string)$yield_from_type
);
return $context;
}
if (BlockAnalysisVisitor::isEmptyIterable($yield_from_type)) {
RedundantCondition::emitInstance(
$node->children['expr'],
$this->code_base,
(clone($this->context))->withLineNumberStart($node->children['expr']->lineno ?? $node->lineno),
Issue::EmptyYieldFrom,
[(string)$yield_from_type],
Closure::fromCallable([BlockAnalysisVisitor::class, 'isEmptyIterable'])
);
}
// Figure out what we intend to return
$method_generator_type = $method->getReturnTypeAsGeneratorTemplateType();
$type_list = $method_generator_type->getTemplateParameterTypeList();
if (\count($type_list) === 0) {
return $context;
}
return $this->compareYieldFromAgainstDeclaredType($node, $method, $context, $type_list, $yield_from_type);
}
/**
* @param list<UnionType> $template_type_list
*/
private function compareYieldFromAgainstDeclaredType(Node $node, FunctionInterface $method, Context $context, array $template_type_list, UnionType $yield_from_type): Context
{
$code_base = $this->code_base;
$type_list_count = \count($template_type_list);
// TODO: Can do a better job of analyzing expressions that are just arrays or subclasses of Traversable.
//
// A solution would need to check for (at)return Generator|T[]
$yield_from_generator_type = $yield_from_type->asGeneratorTemplateType();
$actual_template_type_list = $yield_from_generator_type->getTemplateParameterTypeList();
$actual_type_list_count = \count($actual_template_type_list);
if ($actual_type_list_count === 0) {
return $context;
}
$yield_value_type = $actual_template_type_list[\min(1, $actual_type_list_count - 1)];
$expected_value_type = $template_type_list[\min(1, $type_list_count - 1)];
if (!$yield_value_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_value_type)) {
$this->emitIssue(
Issue::TypeMismatchGeneratorYieldValue,
$node->lineno,
sprintf('(values of %s)', ASTReverter::toShortString($node)),
(string)$yield_value_type,
$method->getNameForIssue(),
(string)$expected_value_type,
'\Generator<' . implode(',', $template_type_list) . '>'
);
}
if ($type_list_count > 1 && $actual_type_list_count > 1) {
// TODO: finalize syntax to indicate the absence of a key or value (e.g. use void instead?)
$yield_key_type = $actual_template_type_list[0];
$expected_key_type = $template_type_list[0];
if (!$yield_key_type->withStaticResolvedInContext($context)->asExpandedTypes($code_base)->canCastToUnionType($expected_key_type)) {
$this->emitIssue(
Issue::TypeMismatchGeneratorYieldKey,
$node->lineno,
sprintf('(keys of %s)', ASTReverter::toShortString($node)),
(string)$yield_key_type,
$method->getNameForIssue(),
(string)$expected_key_type,
'\Generator<' . implode(',', $template_type_list) . '>'
);
}
}
return $context;
}
private function checkCanCastToReturnType(UnionType $expression_type, UnionType $method_return_type): bool
{
if ($expression_type->hasRealTypeSet() && $method_return_type->hasRealTypeSet()) {
$real_expression_type = $expression_type->getRealUnionType();
$real_method_return_type = $method_return_type->getRealUnionType();
if (!$real_method_return_type->isNull() && !$real_expression_type->canCastToDeclaredType($this->code_base, $this->context, $real_method_return_type)) {
return false;
}
}
if ($method_return_type->hasTemplateParameterTypes()) {
// Perform a check that does a better job understanding rules of templates.
// (E.g. should be able to cast None to Option<MyClass>, but not Some<int> to Option<MyClass>
return $expression_type->asExpandedTypesPreservingTemplate($this->code_base)->canCastToUnionTypeHandlingTemplates($method_return_type, $this->code_base) ||
$expression_type->canCastToUnionTypeHandlingTemplates($method_return_type->asExpandedTypesPreservingTemplate($this->code_base), $this->code_base);
}
// We allow base classes to cast to subclasses, and subclasses to cast to base classes,
// but don't allow subclasses to cast to subclasses on a separate branch of the inheritance tree
try {
return $expression_type->asExpandedTypes($this->code_base)->canCastToUnionType($method_return_type) ||
$expression_type->canCastToUnionType($method_return_type->asExpandedTypes($this->code_base));
} catch (RecursionDepthException $_) {
return false;
}
}
/**
* Precondition: checkCanCastToReturnType is false
*/
private function checkCanCastToReturnTypeIfWasNonNullInstead(UnionType $expression_type, UnionType $method_return_type): bool
{
$nonnull_expression_type = $expression_type->nonNullableClone();
if ($nonnull_expression_type === $expression_type || $nonnull_expression_type->isEmpty()) {
return false;
}
return $this->checkCanCastToReturnType($nonnull_expression_type, $method_return_type);
}
/**
* @param Node|string|int|float|null $inner_node
*/
private function analyzeReturnStrict(
CodeBase $code_base,
FunctionInterface $method,
UnionType $expression_type,
UnionType $method_return_type,
int $lineno,
$inner_node
): bool {
$type_set = $expression_type->getTypeSet();
$context = $this->context;
if (\count($type_set) < 2) {
throw new AssertionError("Expected at least two types for strict return type checks");
}
$mismatch_type_set = UnionType::empty();
$mismatch_expanded_types = null;
// For the strict
foreach ($type_set as $type) {
// Expand it to include all parent types up the chain
try {
$individual_type_expanded = $type->asExpandedTypes($code_base);
} catch (RecursionDepthException $_) {
continue;
}
// See if the argument can be cast to the
// parameter
if (!$individual_type_expanded->canCastToUnionType(
$method_return_type
)) {
if ($method->isPHPInternal()) {
// If we are not in strict mode and we accept a string parameter
// and the argument we are passing has a __toString method then it is ok
if (!$context->isStrictTypes() && $method_return_type->hasNonNullStringType()) {
if ($individual_type_expanded->hasClassWithToStringMethod($code_base, $context)) {
continue;
}
}
}
$mismatch_type_set = $mismatch_type_set->withType($type);
if ($mismatch_expanded_types === null) {
// Warn about the first type
$mismatch_expanded_types = $individual_type_expanded;
}
}
}
if ($mismatch_expanded_types === null) {
// No mismatches
return false;
}
// If we have TypeMismatchReturn already, then also suppress the partial mismatch warnings (e.g. PartialTypeMismatchReturn) as well.
if ($this->context->hasSuppressIssue($code_base, Issue::TypeMismatchReturn)) {
return false;
}
$this->emitIssue(
self::getStrictIssueType($mismatch_type_set),
$lineno,
self::returnExpressionToShortString($inner_node),
(string)$expression_type,
$method->getNameForIssue(),
(string)$method_return_type,
$mismatch_expanded_types
);
return true;
}
/**
* @param Node|string|int|float|null $node
*/
private static function returnExpressionToShortString($node): string
{
return $node !== null ? ASTReverter::toShortString($node) : 'void';
}
private static function getStrictIssueType(UnionType $union_type): string
{
if ($union_type->typeCount() === 1) {
$type = $union_type->getTypeSet()[0];
if ($type instanceof NullType) {
return Issue::PossiblyNullTypeReturn;
}
if ($type instanceof FalseType) {
return Issue::PossiblyFalseTypeReturn;
}
}
return Issue::PartialTypeMismatchReturn;
}
/**
* @param ?Node|?string|?int|?float $node
* @return \Generator<int, array{0: UnionType, 1:Node|string|int|float|null}>
*/
private function getReturnTypes(Context $context, $node, int $return_lineno): \Generator
{
if (!($node instanceof Node)) {
if (null === $node) {
yield $return_lineno => [VoidType::instance(false)->asRealUnionType(), null];
return;
}
yield $return_lineno => [
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$context,
$node,
true
),
$node
];
return;
}
$kind = $node->kind;
if ($kind === ast\AST_CONDITIONAL) {
yield from self::deduplicateUnionTypes($this->getReturnTypesOfConditional($context, $node));
return;
} elseif ($kind === ast\AST_ARRAY) {
$expression_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $context, $node, true);
if ($expression_type->hasTopLevelArrayShapeTypeInstances()) {
yield $return_lineno => [$expression_type, $node];
return;
}
// TODO: Infer list<>
$key_type_enum = GenericArrayType::getKeyTypeOfArrayNode($this->code_base, $context, $node);
foreach (self::deduplicateUnionTypes($this->getReturnTypesOfArray($context, $node)) as $return_lineno => [$elem_type, $elem_node]) {
yield $return_lineno => [
$elem_type->asGenericArrayTypes($key_type_enum), // TODO: Infer corresponding key types
$elem_node,
];
}
return;
}
$expression_type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$context,
$node,
true
);
yield $return_lineno => [$expression_type, $node];
}
/**
* @return \Generator|UnionType[]
* @phan-return \Generator<int,array{0:UnionType,1:Node|string|int|float|null}>
*/
private function getReturnTypesOfConditional(Context $context, Node $node): \Generator
{
$cond_node = $node->children['cond'];
$cond_truthiness = UnionTypeVisitor::checkCondUnconditionalTruthiness($cond_node);
// For the shorthand $a ?: $b, the cond node will be the truthy value.
// Note: an ast node will never be null(can be unset), it will be a const AST node with the name null.
$true_node = $node->children['true'] ?? $cond_node;
// Rarely, a conditional will always be true or always be false.
if ($cond_truthiness !== null) {
// TODO: Add no-op checks in another PR, if they don't already exist for conditional.
if ($cond_truthiness) {
// The condition is unconditionally true
yield from $this->getReturnTypes($context, $true_node, $node->lineno);
return;
} else {
// The condition is unconditionally false
// Add the type for the 'false' side
yield from $this->getReturnTypes($context, $node->children['false'], $node->lineno);
return;
}
}
// TODO: false_context once there is a NegatedConditionVisitor
// TODO: emit no-op if $cond_node is a literal, such as `if (2)`
// - Also note that some things such as `true` and `false` are ast\AST_NAME nodes.
if ($cond_node instanceof Node) {
// TODO: Use different contexts and merge those, in case there were assignments or assignments by reference in both sides of the conditional?
// Reuse the BranchScope (sort of unintuitive). The ConditionVisitor returns a clone and doesn't modify the original.
$base_context = $this->context;
// We don't bother analyzing visitReturn in PostOrderAnalysisVisitor, right now.
// This may eventually change, just to ensure the expression is checked for issues
$true_context = (new ConditionVisitor(
$this->code_base,
$base_context
))->__invoke($cond_node);
$false_context = (new NegatedConditionVisitor(
$this->code_base,
$base_context
))->__invoke($cond_node);
} else {
$true_context = $context;
$false_context = $this->context;
}
// Allow nested ternary operators, or arrays within ternary operators
if (isset($node->children['true'])) {
yield from $this->getReturnTypes($true_context, $true_node, $true_node->lineno ?? $node->lineno);
} else {
// E.g. From the left-hand side of yield (int|false) ?: default,
// yielding false is impossible.
foreach ($this->getReturnTypes($true_context, $true_node, $true_node->lineno ?? $node->lineno) as $lineno => $details) {
$raw_union_type = $details[0];
if ($raw_union_type->isEmpty() || !$raw_union_type->containsFalsey()) {
yield $lineno => $details;
} else {
$raw_union_type = $raw_union_type->nonFalseyClone();
if (!$raw_union_type->isEmpty()) {
yield $lineno => [$raw_union_type, $details[1]];
}
}
}
}
$false_node = $node->children['false'];
yield from $this->getReturnTypes($false_context, $false_node, $false_node->lineno ?? $node->lineno);
}
/**
* @param iterable<int, array{0: UnionType, 1: Node|string|int|float|null}> $types
* @return \Generator<int, array{0: UnionType, 1: Node|string|int|float|null}>
* @suppress PhanPluginCanUseParamType should probably suppress, iterable is php 7.2
*/
private static function deduplicateUnionTypes($types): \Generator
{
$unique_types = [];
foreach ($types as $lineno => $details) {
$type = $details[0];
foreach ($unique_types as $old_type) {
if ($type->isEqualTo($old_type)) {
continue 2;
}
}
yield $lineno => $details;
$unique_types[] = $type;
}
}
/**
* @return \Generator|iterable<int,array{0:UnionType,1:Node|int|string|float|null}>
* @phan-return \Generator<int,array{0:UnionType,1:Node|int|string|float|null}>
*/
private function getReturnTypesOfArray(Context $context, Node $node): \Generator
{
if (\count($node->children) === 0) {
// Possibly unreachable (array shape would be returned instead)
yield $node->lineno => [MixedType::instance(false)->asPHPDocUnionType(), $node];
return;
}
foreach ($node->children as $elem) {
if (!($elem instanceof Node)) {
// We already emit PhanSyntaxError
continue;
}
// Don't bother recursing more than one level to iterate over possible types.
if ($elem->kind === \ast\AST_UNPACK) {
// Could optionally recurse to better analyze `yield [...SOME_EXPRESSION_WITH_MIX_OF_VALUES]`
yield $elem->lineno => [
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$context,
$elem,
true
),
$elem
];
continue;
}
$value_node = $elem->children['value'];
if ($value_node instanceof Node) {
yield $elem->lineno => [
UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$context,
$value_node,
true
),
$value_node
];
} else {
yield $elem->lineno => [
Type::fromObject($value_node)->asRealUnionType(),
$value_node
];
}
}
}
/**
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitPropDecl(Node $node): Context
{
return $this->context;
}
/**
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitPropGroup(Node $node): Context
{
$this->checkUnionTypeCompatibility($node->children['type']);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitCall(Node $node): Context
{
$expression = $node->children['expr'];
try {
// Get the function.
// If the function is undefined, always try to create a placeholder from Phan's type signatures for internal functions so they can still be type checked.
$function_list_generator = (new ContextNode(
$this->code_base,
$this->context,
$expression
))->getFunctionFromNode(true);
foreach ($function_list_generator as $function) {
// Check the call for parameter and argument types
$this->analyzeCallToFunctionLike(
$function,
$node
);
if ($function instanceof Func && \strcasecmp($function->getName(), 'assert') === 0 && $function->getFQSEN()->getNamespace() === '\\') {
$this->context = $this->analyzeAssert($this->context, $node);
}
}
} catch (CodeBaseException $_) {
// ignore it.
}
return $this->context;
}
private function analyzeAssert(Context $context, Node $node): Context
{
$args_first_child = $node->children['args']->children[0] ?? null;
if (!($args_first_child instanceof Node)) {
return $this->context;
}
// Look to see if the asserted expression says anything about
// the types of any variables.
return (new ConditionVisitor(
$this->code_base,
$context
))->__invoke($args_first_child);
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitNew(Node $node): Context
{
$class_list = [];
try {
$context_node = new ContextNode(
$this->code_base,
$this->context,
$node
);
$method = $context_node->getMethod(
'__construct',
false,
false,
true
);
$class_list = $context_node->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME);
// Add a reference to each class this method
// could be called on
foreach ($class_list as $class) {
$class->addReference($this->context);
if ($class->isDeprecated()) {
$this->emitIssue(
Issue::DeprecatedClass,
$node->lineno,
(string)$class->getFQSEN(),
$class->getContext()->getFile(),
(string)$class->getContext()->getLineNumberStart(),
$class->getDeprecationReason()
);
}
}
$this->analyzeMethodVisibility(
$method,
$node
);
$this->analyzeCallToFunctionLike(
$method,
$node
);
foreach ($class_list as $class) {
if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) {
// Check the full list of classes if any of the classes
// are abstract or interfaces.
$this->checkForInvalidNewType($node, $class_list);
break;
}
}
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// If we can't figure out what kind of a call
// this is, don't worry about it
}
if ($this->isInNoOpPosition($node)) {
$this->warnNoopNew($node, $class_list);
}
return $this->context;
}
/**
* @param Node $node a node of type AST_NEW
* @param Clazz[] $class_list
*/
private function checkForInvalidNewType(Node $node, array $class_list): void
{
// This is either a string (new 'something'()) or a class name (new something())
$class_node = $node->children['class'];
if (!$class_node instanceof Node) {
foreach ($class_list as $class) {
$this->warnIfInvalidClassForNew($class, $node);
}
return;
}
if ($class_node->kind === ast\AST_NAME) {
$class_name = $class_node->children['name'];
if (\is_string($class_name) && \strcasecmp('static', $class_name) === 0) {
if ($this->isStaticGuaranteedToBeNonAbstract()) {
return;
}
}
foreach ($class_list as $class) {
$this->warnIfInvalidClassForNew($class, $class_node);
}
return;
}
foreach (UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $class_node)->getTypeSet() as $type) {
if ($type instanceof LiteralStringType) {
try {
$class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($type->getValue());
} catch (FQSENException $_) {
// Probably already emitted elsewhere, but emit anyway
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeExpectedObjectOrClassName,
$node->lineno,
ASTReverter::toShortString($node),
$type->getValue()
);
continue;
}
if (!$this->code_base->hasClassWithFQSEN($class_fqsen)) {
continue;
}
$class = $this->code_base->getClassByFQSEN($class_fqsen);
$this->warnIfInvalidClassForNew($class, $class_node);
}
}
}
/**
* Given a call to `new static`, is the context likely to be guaranteed to be a non-abstract class?
*/
private function isStaticGuaranteedToBeNonAbstract(): bool
{
if (!$this->context->isInMethodScope()) {
return false;
}
// TODO: Could do a better job with closures inside of methods
$method = $this->context->getFunctionLikeInScope($this->code_base);
if (!($method instanceof Method)) {
if ($method instanceof Func && $method->isClosure()) {
// closures can be rebound
return true;
}
return false;
}
return !$method->isStatic();
}
private static function isStaticNameNode(Node $node, bool $allow_self): bool
{
if ($node->kind !== ast\AST_NAME) {
return false;
}
$name = $node->children['name'];
if (!\is_string($name)) {
return false;
}
return \strcasecmp($name, 'static') === 0 || ($allow_self && \strcasecmp($name, 'self') === 0);
}
private function warnIfInvalidClassForNew(Clazz $class, Node $node): void
{
// Make sure we're not instantiating an abstract
// class
if ($class->isAbstract()) {
$this->emitIssue(
self::isStaticNameNode($node, false) ? Issue::TypeInstantiateAbstractStatic : Issue::TypeInstantiateAbstract,
$node->lineno,
(string)$class->getFQSEN()
);
} elseif ($class->isInterface()) {
// Make sure we're not instantiating an interface
$this->emitIssue(
Issue::TypeInstantiateInterface,
$node->lineno,
(string)$class->getFQSEN()
);
} elseif ($class->isTrait()) {
// Make sure we're not instantiating a trait
$this->emitIssue(
self::isStaticNameNode($node, true) ? Issue::TypeInstantiateTraitStaticOrSelf : Issue::TypeInstantiateTrait,
$node->lineno,
(string)$class->getFQSEN()
);
}
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitInstanceof(Node $node): Context
{
try {
// Fetch the class list, and emit warnings as a side effect.
// TODO: Unify UnionTypeVisitor, AssignmentVisitor, and PostOrderAnalysisVisitor
(new ContextNode(
$this->code_base,
$this->context,
$node->children['class']
))->getClassList(false, ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME, Issue::TypeInvalidInstanceof);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (CodeBaseException $exception) {
$this->emitIssueWithSuggestion(
Issue::UndeclaredClassInstanceof,
$node->lineno,
[(string)$exception->getFQSEN()],
IssueFixSuggester::suggestSimilarClassForGenericFQSEN(
$this->code_base,
$this->context,
$exception->getFQSEN(),
// Only suggest classes/interfaces for alternatives to instanceof checks. Don't suggest traits.
IssueFixSuggester::createFQSENFilterForClasslikeCategories($this->code_base, true, false, true)
)
);
}
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitStaticCall(Node $node): Context
{
// Get the name of the method being called
$method_name = $node->children['method'];
// Give up on things like Class::$var
if (!\is_string($method_name)) {
if ($method_name instanceof Node) {
$method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name);
}
if (!\is_string($method_name)) {
$method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['method']);
if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeInvalidStaticMethodName,
$node->lineno,
$method_name_type
);
}
return $this->context;
}
}
// Get the name of the static class being referenced
$static_class = '';
$class_node = $node->children['class'];
if (!($class_node instanceof Node)) {
$static_class = (string)$class_node;
} elseif ($class_node->kind === ast\AST_NAME) {
$static_class = (string)$class_node->children['name'];
}
$method = $this->getStaticMethodOrEmitIssue($node, $method_name);
if ($method === null) {
// Short circuit on a constructor being called statically
// on something other than 'parent'
if ($method_name === '__construct' && $static_class !== 'parent') {
$this->emitConstructorWarning($node, $static_class, $method_name);
}
return $this->context;
}
try {
if ($method_name === '__construct') {
$this->checkNonAncestorConstructCall($node, $static_class, $method_name);
// Even if it exists, continue on and type check the arguments passed.
}
// If the method being called isn't actually static and it's
// not a call to parent::f from f, we may be in trouble.
if (!$method->isStatic() && !$this->canCallInstanceMethodFromContext($method, $static_class)) {
$class_list = (new ContextNode(
$this->code_base,
$this->context,
$node->children['class']
))->getClassList();
if (\count($class_list) > 0) {
$class = \array_values($class_list)[0];
$this->emitIssue(
Issue::StaticCallToNonStatic,
$node->lineno,
"{$class->getFQSEN()}::{$method_name}()",
$method->getFileRef()->getFile(),
(string)$method->getFileRef()->getLineNumberStart()
);
}
}
$this->analyzeMethodVisibility(
$method,
$node
);
// Make sure the parameters look good
$this->analyzeCallToFunctionLike(
$method,
$node
);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// If we can't figure out the class for this method
// call, cry YOLO and mark every method with that
// name with a reference.
if (Config::get_track_references()
&& Config::getValue('dead_code_detection_prefer_false_negative')
) {
foreach ($this->code_base->getMethodSetByName(
$method_name
) as $method) {
$method->addReference($this->context);
}
}
// If we can't figure out what kind of a call
// this is, don't worry about it
return $this->context;
}
return $this->context;
}
private function canCallInstanceMethodFromContext(Method $method, string $static_class): bool
{
// Check if this is an instance method or closure of an instance method
if (!$this->context->getScope()->hasVariableWithName('this')) {
return false;
}
if (\in_array(\strtolower($static_class), ['parent', 'self', 'static'], true)) {
return true;
}
$calling_class_fqsen = $this->context->getClassFQSENOrNull();
if ($calling_class_fqsen) {
$calling_class_type = $calling_class_fqsen->asType()->asExpandedTypes($this->code_base);
} else {
$calling_class_type = $this->context->getScope()->getVariableByName('this')->getUnionType()->asExpandedTypes($this->code_base);
}
// Allow calling its own methods and class's methods.
return $calling_class_type->hasType($method->getClassFQSEN()->asType());
}
/**
* Check calling A::__construct (where A is not parent)
*/
private function checkNonAncestorConstructCall(
Node $node,
string $static_class,
string $method_name
): void {
// TODO: what about unanalyzable?
if ($node->children['class']->kind !== ast\AST_NAME) {
return;
}
// TODO: check for self/static/<class name of self> and warn about recursion?
// TODO: Only allow calls to __construct from other constructors?
$found_ancestor_constructor = false;
if ($this->context->isInMethodScope()) {
try {
$possible_ancestor_type = UnionTypeVisitor::unionTypeFromClassNode(
$this->code_base,
$this->context,
$node->children['class']
);
} catch (FQSENException $e) {
$this->emitIssue(
$e instanceof EmptyFQSENException ? Issue::EmptyFQSENInCallable : Issue::InvalidFQSENInCallable,
$node->lineno,
$e->getFQSEN()
);
return;
}
// If we can determine the ancestor type, and it's an parent/ancestor class, allow the call without warning.
// (other code should check visibility and existence and args of __construct)
if (!$possible_ancestor_type->isEmpty()) {
// but forbid 'self::__construct', 'static::__construct'
$type = $this->context->getClassFQSEN()->asRealUnionType();
if ($possible_ancestor_type->hasStaticType()) {
$this->emitIssue(
Issue::AccessOwnConstructor,
$node->lineno,
$static_class
);
$found_ancestor_constructor = true;
} elseif ($type->asExpandedTypes($this->code_base)->canCastToUnionType($possible_ancestor_type)) {
if ($type->canCastToUnionType($possible_ancestor_type)) {
$this->emitIssue(
Issue::AccessOwnConstructor,
$node->lineno,
$static_class
);
}
$found_ancestor_constructor = true;
}
}
}
if (!$found_ancestor_constructor) {
// TODO: new issue type?
$this->emitConstructorWarning($node, $static_class, $method_name);
}
}
/**
* TODO: change to a different issue type in a future phan release?
*/
private function emitConstructorWarning(Node $node, string $static_class, string $method_name): void
{
$this->emitIssue(
Issue::UndeclaredStaticMethod,
$node->lineno,
"{$static_class}::{$method_name}()"
);
}
/**
* gets the static method, or emits an issue.
* @param Node $node
* @param string $method_name - NOTE: The caller should convert constants/class constants/etc in $node->children['method'] to a string.
*/
private function getStaticMethodOrEmitIssue(Node $node, string $method_name): ?Method
{
try {
// Get a reference to the method being called
$result = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, true, true);
// This didn't throw NonClassMethodCall
if (Config::get_strict_method_checking()) {
$this->checkForPossibleNonObjectAndNonClassInMethod($node, $method_name);
}
return $result;
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $e) {
if ($e instanceof FQSENException) {
Issue::maybeEmit(
$this->code_base,
$this->context,
$e instanceof EmptyFQSENException ? Issue::EmptyFQSENInClasslike : Issue::InvalidFQSENInClasslike,
$node->lineno,
$e->getFQSEN()
);
}
// We already checked for NonClassMethodCall
if (Config::get_strict_method_checking()) {
$this->checkForPossibleNonObjectAndNonClassInMethod($node, $method_name);
}
// If we can't figure out the class for this method
// call, cry YOLO and mark every method with that
// name with a reference.
if (Config::get_track_references()
&& Config::getValue('dead_code_detection_prefer_false_negative')
) {
foreach ($this->code_base->getMethodSetByName(
$method_name
) as $method) {
$method->addReference($this->context);
}
}
// If we can't figure out what kind of a call
// this is, don't worry about it
}
return null;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitMethod(Node $node): Context
{
if (!$this->context->isInFunctionLikeScope()) {
throw new AssertionError("Must be in function-like scope to get method");
}
$method = $this->context->getFunctionLikeInScope($this->code_base);
$return_type = $method->getUnionType();
if (!($method instanceof Method)) {
throw new AssertionError("Function found where method expected");
}
$has_interface_class = false;
try {
$class = $method->getClass($this->code_base);
$has_interface_class = $class->isInterface();
$this->checkForPHP4StyleConstructor($class, $method);
} catch (Exception $_) {
}
if (!$method->isAbstract()
&& !$method->isFromPHPDoc()
&& !$has_interface_class
&& !$return_type->isEmpty()
&& !$method->hasReturn()
&& !self::declOnlyThrows($node)
&& !$return_type->hasType(VoidType::instance(false))
&& !$return_type->hasType(NullType::instance(false))
) {
$this->warnTypeMissingReturn($method, $node);
}
$this->checkForFunctionInterfaceIssues($node, $method);
if ($method->hasReturn() && $method->isMagicAndVoid()) {
$this->emitIssue(
Issue::TypeMagicVoidWithReturn,
$node->lineno,
(string)$method->getFQSEN()
);
}
return $this->context;
}
private function warnTypeMissingReturn(FunctionInterface $method, Node $node): void
{
$this->emitIssue(
$method->getRealReturnType()->isEmpty() ? Issue::TypeMissingReturn : Issue::TypeMissingReturnReal,
$node->lineno,
$method->getFQSEN(),
$method->getUnionType()
);
}
/**
* Visit a node with kind `ast\AST_FUNC_DECL`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitFuncDecl(Node $node): Context
{
$method =
$this->context->getFunctionLikeInScope($this->code_base);
if (\strcasecmp($method->getName(), '__autoload') === 0) {
$this->emitIssue(
Issue::CompatibleAutoload,
$node->lineno
);
}
$return_type = $method->getUnionType();
if (!$return_type->isEmpty()
&& !$method->hasReturn()
&& !self::declOnlyThrows($node)
&& !$return_type->hasType(VoidType::instance(false))
&& !$return_type->hasType(NullType::instance(false))
) {
$this->warnTypeMissingReturn($method, $node);
}
$this->checkForFunctionInterfaceIssues($node, $method);
return $this->context;
}
/**
* @suppress PhanPossiblyUndeclaredProperty
*/
private function checkForFunctionInterfaceIssues(Node $node, FunctionInterface $function): void
{
$parameters_seen = [];
foreach ($function->getParameterList() as $i => $parameter) {
if (isset($parameters_seen[$parameter->getName()])) {
$this->emitIssue(
Issue::ParamRedefined,
$node->lineno,
'$' . $parameter->getName()
);
} else {
$parameters_seen[$parameter->getName()] = $i;
}
}
$params_node = $node->children['params'];
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($params_node->polyfill_has_trailing_comma)) {
$this->emitIssue(
Issue::CompatibleTrailingCommaParameterList,
end($params_node->children)->lineno ?? $params_node->lineno,
ASTReverter::toShortString($node)
);
}
foreach ($params_node->children as $param) {
$this->checkUnionTypeCompatibility($param->children['type']);
}
$this->checkUnionTypeCompatibility($node->children['returnType']);
}
private function checkUnionTypeCompatibility(?Node $type): void
{
if (!$type) {
return;
}
if (Config::get_closest_minimum_target_php_version_id() >= 80000) {
// Don't warn about using union types if the project dropped support for php versions older than 8.0
return;
}
if ($type->kind === ast\AST_TYPE_UNION) {
// TODO: Warn about false|false, false|null, etc in php 8.0.
$this->emitIssue(
Issue::CompatibleUnionType,
$type->lineno,
ASTReverter::toShortString($type)
);
return;
}
if ($type->kind === ast\AST_NULLABLE_TYPE) {
$inner_type = $type->children['type'];
if (!\is_object($inner_type)) {
// The polyfill will create param type nodes for function(? $x)
// Phan warns elsewhere.
return;
}
} else {
$inner_type = $type;
}
// echo \Phan\Debug::nodeToString($type) . "\n";
if ($inner_type->kind === ast\AST_NAME) {
return;
}
if ($inner_type->kind !== ast\AST_TYPE) {
// e.g. ast\TYPE_UNION
$this->emitIssue(
Issue::InvalidNode,
$inner_type->lineno,
"Unsupported union type syntax " . ASTReverter::toShortString($inner_type)
);
return;
}
if ($inner_type->flags === ast\flags\TYPE_STATIC) {
$this->emitIssue(
Issue::CompatibleStaticType,
$inner_type->lineno
);
} elseif (\in_array($inner_type->flags, [ast\flags\TYPE_FALSE, ast\flags\TYPE_NULL], true)) {
$this->emitIssue(
Issue::InvalidNode,
$inner_type->lineno,
"Invalid union type " . ASTReverter::toShortTypeString($type) . " in element signature"
);
}
}
public function visitNullsafeMethodCall(Node $node): Context
{
$this->checkNullsafeOperatorCompatibility($node);
return $this->visitMethodCall($node);
}
private function checkNullsafeOperatorCompatibility(Node $node): void
{
if (Config::get_closest_minimum_target_php_version_id() < 80000) {
$this->emitIssue(
Issue::CompatibleNullsafeOperator,
$node->lineno,
ASTReverter::toShortString($node)
);
}
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitMethodCall(Node $node): Context
{
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
if ($method_name instanceof Node) {
$method_name = UnionTypeVisitor::anyStringLiteralForNode($this->code_base, $this->context, $method_name);
}
if (!\is_string($method_name)) {
$method_name_type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['method']);
if (!$method_name_type->canCastToUnionType(StringType::instance(false)->asPHPDocUnionType())) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::TypeInvalidMethodName,
$node->lineno,
$method_name_type
);
}
return $this->context;
}
}
try {
$method = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, false, true);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
return $this->context;
} catch (NodeException $_) {
// If we can't figure out the class for this method
// call, cry YOLO and mark every method with that
// name with a reference.
if (Config::get_track_references()
&& Config::getValue('dead_code_detection_prefer_false_negative')
) {
foreach ($this->code_base->getMethodSetByName(
$method_name
) as $method) {
$method->addReference($this->context);
}
}
// Swallow it
return $this->context;
}
// We already checked for NonClassMethodCall
if (Config::get_strict_method_checking()) {
$this->checkForPossibleNonObjectInMethod($node, $method_name);
}
$this->analyzeMethodVisibility(
$method,
$node
);
// Check the call for parameter and argument types
$this->analyzeCallToFunctionLike(
$method,
$node
);
return $this->context;
}
/**
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitArgList(Node $node): Context
{
$argument_name_set = [];
$has_unpack = false;
foreach ($node->children as $i => $argument) {
if (!\is_int($i)) {
throw new AssertionError("Expected argument index to be an integer");
}
if ($argument instanceof Node && $argument->kind === ast\AST_NAMED_ARG) {
if (Config::get_closest_minimum_target_php_version_id() < 80000) {
$this->emitIssue(
Issue::CompatibleNamedArgument,
$argument->lineno,
ASTReverter::toShortString($argument)
);
}
['name' => $argument_name, 'expr' => $argument_expression] = $argument->children;
if ($argument_expression === null) {
throw new AssertionError("Expected argument to have an expression");
}
if (isset($argument_name_set[$argument_name])) {
$this->emitIssue(
Issue::DefinitelyDuplicateNamedArgument,
$argument->lineno,
ASTReverter::toShortString($argument),
ASTReverter::toShortString($argument_name_set[$argument_name])
);
} else {
$argument_name_set[$argument_name] = $argument;
}
} else {
$argument_expression = $argument;
}
if ($argument_name_set) {
if ($argument === $argument_expression) {
$this->emitIssue(
Issue::PositionalArgumentAfterNamedArgument,
$argument->lineno ?? $node->lineno,
ASTReverter::toShortString($argument),
ASTReverter::toShortString(\end($argument_name_set))
);
}
}
if (($argument->kind ?? 0) === ast\AST_UNPACK) {
$has_unpack = true;
}
}
// TODO: Make this a check that runs even without the $method object
if ($has_unpack && $argument_name_set) {
$this->emitIssue(
Issue::ArgumentUnpackingUsedWithNamedArgument,
$node->lineno,
ASTReverter::toShortString($node)
);
}
// @phan-suppress-next-line PhanUndeclaredProperty
if (isset($node->polyfill_has_trailing_comma) && Config::get_closest_minimum_target_php_version_id() < 70300) {
$this->emitIssue(
Issue::CompatibleTrailingCommaArgumentList,
end($node->children)->lineno ?? $node->lineno,
ASTReverter::toShortString($node)
);
}
return $this->context;
}
private function checkForPossibleNonObjectInMethod(Node $node, string $method_name): void
{
$type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class']);
if ($node->kind === ast\AST_NULLSAFE_METHOD_CALL && !$type->isNull() && !$type->isDefinitelyUndefined()) {
$type = $type->nonNullableClone();
}
if ($type->containsDefiniteNonObjectType()) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::PossiblyNonClassMethodCall,
$node->lineno,
$method_name,
$type
);
}
}
private function checkForPossibleNonObjectAndNonClassInMethod(Node $node, string $method_name): void
{
$type = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $node->children['expr'] ?? $node->children['class']);
if ($type->containsDefiniteNonObjectAndNonClassType()) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::PossiblyNonClassMethodCall,
$node->lineno,
$method_name,
$type
);
}
}
/**
* Visit a node with kind `ast\AST_DIM`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitDim(Node $node): Context
{
$code_base = $this->code_base;
$context = $this->context;
// Check the dimension type to trigger PhanUndeclaredVariable, etc.
/* $dim_type = */
UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$node->children['dim'],
true
);
$this->analyzeNoOp($node, Issue::NoopArrayAccess);
$flags = $node->flags;
if ($flags & ast\flags\DIM_ALTERNATIVE_SYNTAX) {
$this->emitIssue(
Issue::CompatibleDimAlternativeSyntax,
$node->children['dim']->lineno ?? $node->lineno,
ASTReverter::toShortString($node)
);
}
if ($flags & PhanAnnotationAdder::FLAG_IGNORE_NULLABLE_AND_UNDEF) {
return $context;
}
// Check the array type to trigger TypeArraySuspicious
try {
/* $array_type = */
UnionTypeVisitor::unionTypeFromNode(
$code_base,
$context,
$node,
false
);
// TODO: check if array_type has array but not ArrayAccess.
// If that is true, then assert that $dim_type can cast to `int|string`
} catch (IssueException $_) {
// Detect this elsewhere, e.g. want to detect PhanUndeclaredVariableDim but not PhanUndeclaredVariable
}
return $context;
}
/**
* Visit a node with kind `ast\AST_CONDITIONAL`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*
* @suppress PhanAccessMethodInternal
*/
public function visitConditional(Node $node): Context
{
if ($this->isInNoOpPosition($node)) {
if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['true']) &&
!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['false'])) {
$this->emitIssue(
Issue::NoopTernary,
$node->lineno
);
}
}
$cond = $node->children['cond'];
if ($cond instanceof Node && $cond->kind === ast\AST_CONDITIONAL) {
$this->checkDeprecatedUnparenthesizedConditional($node, $cond);
}
return $this->context;
}
/**
* Visit a node with kind `ast\AST_MATCH`
*
* @param Node $node
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* analyzing the node
*
* @suppress PhanAccessMethodInternal
*/
public function visitMatch(Node $node): Context
{
if (Config::get_closest_minimum_target_php_version_id() < 80000) {
$this->emitIssue(
Issue::CompatibleMatchExpression,
$node->lineno,
ASTReverter::toShortString($node)
);
}
if ($this->isInNoOpPosition($node)) {
if (!ScopeImpactCheckingVisitor::hasPossibleImpact($this->code_base, $this->context, $node->children['stmts'])) {
$this->emitIssue(
Issue::NoopMatchExpression,
$node->lineno,
ASTReverter::toShortString($node)
);
}
}
return $this->context;
}
/**
* @param Node $node a node of kind AST_CONDITIONAL with a condition that is also of kind AST_CONDITIONAL
*/
private function checkDeprecatedUnparenthesizedConditional(Node $node, Node $cond): void
{
if ($cond->flags & flags\PARENTHESIZED_CONDITIONAL) {
// The condition is unambiguously parenthesized.
return;
}
// @phan-suppress-next-line PhanUndeclaredProperty
if (\PHP_VERSION_ID < 70400 && !isset($cond->is_not_parenthesized)) {
// This is from the native parser in php 7.3 or earlier.
// We don't know whether or not the AST is parenthesized.
return;
}
if (isset($cond->children['true'])) {
if (isset($node->children['true'])) {
$description = 'a ? b : c ? d : e';
$first_suggestion = '(a ? b : c) ? d : e';
$second_suggestion = 'a ? b : (c ? d : e)';
} else {
$description = 'a ? b : c ?: d';
$first_suggestion = '(a ? b : c) ?: d';
$second_suggestion = 'a ? b : (c ?: d)';
}
} else {
if (isset($node->children['true'])) {
$description = 'a ?: b ? c : d';
$first_suggestion = '(a ?: b) ? c : d';
$second_suggestion = 'a ?: (b ? c : d)';
} else {
// This is harmless - (a ?: b) ?: c always produces the same result and side
// effects as a ?: (b ?: c).
// Don't warn.
return;
}
}
$this->emitIssue(
Issue::CompatibleUnparenthesizedTernary,
$node->lineno,
$description,
$first_suggestion,
$second_suggestion
);
}
/**
* @param list<Node> $parent_node_list
* @return bool true if the union type should skip analysis due to being the left-hand side expression of an assignment
* We skip checks for $x['key'] being valid in expressions such as `$x['key']['key2']['key3'] = 'value';`
* because those expressions will create $x['key'] as a side effect.
*
* Precondition: $parent_node->kind === ast\AST_DIM && $parent_node->children['expr'] is $node
*/
private static function shouldSkipNestedAssignDim(array $parent_node_list): bool
{
$cur_parent_node = \end($parent_node_list);
for (;; $cur_parent_node = $prev_parent_node) {
$prev_parent_node = \prev($parent_node_list);
if (!$prev_parent_node instanceof Node) {
throw new AssertionError('Unexpected end of parent nodes seen in ' . __METHOD__);
}
switch ($prev_parent_node->kind) {
case ast\AST_DIM:
if ($prev_parent_node->children['expr'] !== $cur_parent_node) {
return false;
}
break;
case ast\AST_ASSIGN:
case ast\AST_ASSIGN_REF:
return $prev_parent_node->children['var'] === $cur_parent_node;
case ast\AST_ARRAY_ELEM:
$prev_parent_node = \prev($parent_node_list); // this becomes AST_ARRAY
break;
case ast\AST_ARRAY:
break;
default:
return false;
}
}
}
public function visitStaticProp(Node $node): Context
{
return $this->analyzeProp($node, true);
}
public function visitProp(Node $node): Context
{
return $this->analyzeProp($node, false);
}
public function visitNullsafeProp(Node $node): Context
{
$this->checkNullsafeOperatorCompatibility($node);
return $this->analyzeProp($node, false);
}
/**
* Default visitor for node kinds that do not have
* an overriding method
*
* @param Node $node (@phan-unused-param)
* A node to parse
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitClone(Node $node): Context
{
$type = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$node->children['expr'],
true
);
if ($type->isEmpty()) {
return $this->context;
}
if (!$type->hasPossiblyObjectTypes()) {
$this->emitIssue(
Issue::TypeInvalidCloneNotObject,
$node->children['expr']->lineno ?? $node->lineno,
$type
);
} elseif (Config::get_strict_param_checking()) {
if ($type->containsNullable() || !$type->canStrictCastToUnionType($this->code_base, ObjectType::instance(false)->asPHPDocUnionType())) {
$this->emitIssue(
Issue::TypePossiblyInvalidCloneNotObject,
$node->children['expr']->lineno ?? $node->lineno,
$type
);
}
}
return $this->context;
}
/**
* Analyze a node with kind `ast\AST_PROP` or `ast\AST_STATIC_PROP`
*
* @param Node $node
* A node of the type indicated by the method name that we'd
* like to figure out the type that it produces.
*
* @param bool $is_static
* True if fetching a static property.
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function analyzeProp(Node $node, bool $is_static): Context
{
$exception_or_null = null;
try {
$property = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getProperty($is_static);
// Mark that this property has been referenced from
// this context
if (Config::get_track_references()) {
$this->trackPropertyReference($property, $node);
}
} catch (IssueException $exception) {
// We'll check out some reasons it might not exist
// before logging the issue
$exception_or_null = $exception;
} catch (Exception $_) {
// Swallow any exceptions. We'll catch it later.
}
if (isset($property)) {
// TODO could be more specific about checking if this is a magic property
// Right now it warns if it is magic but (at)property is used, etc.
$this->analyzeNoOp($node, Issue::NoopProperty);
} else {
$expr_or_class_node = $node->children['expr'] ?? $node->children['class'];
if ($expr_or_class_node === null) {
throw new AssertionError(
"Property nodes must either have an expression or class"
);
}
$class_list = [];
try {
// Get the set of classes that are being referenced
$class_list = (new ContextNode(
$this->code_base,
$this->context,
$expr_or_class_node
))->getClassList(
true,
$is_static ? ContextNode::CLASS_LIST_ACCEPT_OBJECT_OR_CLASS_NAME : ContextNode::CLASS_LIST_ACCEPT_OBJECT,
null,
false
);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
}
if (!$is_static) {
// Find out of any of them have a __get magic method
// (Only check if looking for instance properties)
$has_getter = $this->hasGetter($class_list);
// If they don't, then analyze for No-ops.
if (!$has_getter) {
$this->analyzeNoOp($node, Issue::NoopProperty);
if ($exception_or_null instanceof IssueException) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception_or_null->getIssueInstance()
);
}
}
} else {
if ($exception_or_null instanceof IssueException) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception_or_null->getIssueInstance()
);
}
}
}
return $this->context;
}
/** @param Clazz[] $class_list */
private function hasGetter(array $class_list): bool
{
foreach ($class_list as $class) {
if ($class->hasGetMethod($this->code_base)) {
return true;
}
}
return false;
}
private function trackPropertyReference(Property $property, Node $node): void
{
$property->addReference($this->context);
if (!$property->hasReadReference() && !$this->isAssignmentOrNestedAssignment($node)) {
$property->setHasReadReference();
}
if (!$property->hasWriteReference() && $this->isAssignmentOrNestedAssignmentOrModification($node) !== false) {
$property->setHasWriteReference();
}
}
/**
* @return ?bool
* - false if this is a read reference
* - false for modifications such as $x++
* - true if this is a write reference
* - null if this is both, e.g. $a =& $b for $a and $b
*/
private function isAssignmentOrNestedAssignment(Node $node): ?bool
{
$parent_node_list = $this->parent_node_list;
$parent_node = \end($parent_node_list);
if (!$parent_node instanceof Node) {
// impossible
return false;
}
$parent_kind = $parent_node->kind;
// E.g. analyzing [$x] in [$x] = expr()
while ($parent_kind === ast\AST_ARRAY_ELEM) {
if ($parent_node->children['value'] !== $node) {
// e.g. analyzing `$v = [$x => $y];` for $x
return false;
}
\array_pop($parent_node_list); // pop AST_ARRAY_ELEM
$node = \array_pop($parent_node_list); // AST_ARRAY
$parent_node = \array_pop($parent_node_list);
if (!$parent_node instanceof Node) {
// impossible
return false;
}
$parent_kind = $parent_node->kind;
}
if ($parent_kind === ast\AST_DIM) {
return $parent_node->children['expr'] === $node && $this->shouldSkipNestedAssignDim($parent_node_list);
} elseif ($parent_kind === ast\AST_ASSIGN || $parent_kind === ast\AST_ASSIGN_OP) {
return $parent_node->children['var'] === $node;
} elseif ($parent_kind === ast\AST_ASSIGN_REF) {
return null;
}
return false;
}
// An incomplete list of known parent node kinds that simultaneously read and write the given expression
// TODO: ASSIGN_OP?
private const READ_AND_WRITE_KINDS = [
ast\AST_PRE_INC,
ast\AST_PRE_DEC,
ast\AST_POST_INC,
ast\AST_POST_DEC,
];
/**
* @return ?bool
* - false if this is a read reference
* - true if this is a write reference
* - true if this is a modification such as $x++
* - null if this is both, e.g. $a =& $b for $a and $b
*/
private function isAssignmentOrNestedAssignmentOrModification(Node $node): ?bool
{
$parent_node_list = $this->parent_node_list;
$parent_node = \end($parent_node_list);
if (!$parent_node instanceof Node) {
// impossible
return false;
}
$parent_kind = $parent_node->kind;
// E.g. analyzing [$x] in [$x] = expr()
while ($parent_kind === ast\AST_ARRAY_ELEM) {
if ($parent_node->children['value'] !== $node) {
// e.g. analyzing `$v = [$x => $y];` for $x
return false;
}
\array_pop($parent_node_list); // pop AST_ARRAY_ELEM
$node = \array_pop($parent_node_list); // AST_ARRAY
$parent_node = \array_pop($parent_node_list);
if (!$parent_node instanceof Node) {
// impossible
return false;
}
$parent_kind = $parent_node->kind;
}
if ($parent_kind === ast\AST_DIM) {
return $parent_node->children['expr'] === $node && self::shouldSkipNestedAssignDim($parent_node_list);
} elseif ($parent_kind === ast\AST_ASSIGN || $parent_kind === ast\AST_ASSIGN_OP) {
return $parent_node->children['var'] === $node;
} elseif ($parent_kind === ast\AST_ASSIGN_REF) {
return null;
} else {
return \in_array($parent_kind, self::READ_AND_WRITE_KINDS, true);
}
}
/**
* Analyze whether a method is callable
*
* @param Method $method
* @param Node $node
*/
private function analyzeMethodVisibility(
Method $method,
Node $node
): void {
if ($method->isPublic()) {
return;
}
if ($method->isAccessibleFromClass($this->code_base, $this->context->getClassFQSENOrNull())) {
return;
}
if ($method->isPrivate()) {
$has_call_magic_method = !$method->isStatic()
&& $method->getDefiningClass($this->code_base)->hasMethodWithName($this->code_base, '__call', true);
$this->emitIssue(
$has_call_magic_method ?
Issue::AccessMethodPrivateWithCallMagicMethod : Issue::AccessMethodPrivate,
$node->lineno,
(string)$method->getFQSEN(),
$method->getFileRef()->getFile(),
(string)$method->getFileRef()->getLineNumberStart()
);
} else {
if (Clazz::isAccessToElementOfThis($node)) {
return;
}
$has_call_magic_method = !$method->isStatic()
&& $method->getDefiningClass($this->code_base)->hasMethodWithName($this->code_base, '__call', true);
$this->emitIssue(
$has_call_magic_method ?
Issue::AccessMethodProtectedWithCallMagicMethod : Issue::AccessMethodProtected,
$node->lineno,
(string)$method->getFQSEN(),
$method->getFileRef()->getFile(),
(string)$method->getFileRef()->getLineNumberStart()
);
}
}
/**
* Analyze the parameters and arguments for a call
* to the given method or function
*
* @param FunctionInterface $method
* @param Node $node
*/
private function analyzeCallToFunctionLike(
FunctionInterface $method,
Node $node
): void {
$code_base = $this->code_base;
$context = $this->context;
$method->addReference($context);
// Create variables for any pass-by-reference
// parameters
$argument_list = $node->children['args']->children;
foreach ($argument_list as $i => $argument) {
if (!$argument instanceof Node) {
continue;
}
$parameter = $method->getParameterForCaller($i);
if (!$parameter) {
continue;
}
// If pass-by-reference, make sure the variable exists
// or create it if it doesn't.
if ($parameter->isPassByReference()) {
$this->createPassByReferenceArgumentInCall($method, $argument, $parameter, $method->getRealParameterForCaller($i));
}
}
// Confirm the argument types are clean
ArgumentType::analyze(
$method,
$node,
$context,
$code_base
);
// Take another pass over pass-by-reference parameters
// and assign types to passed in variables
foreach ($argument_list as $i => $argument) {
if (!$argument instanceof Node) {
continue;
}
$parameter = $method->getParameterForCaller($i);
if (!$parameter) {
continue;
}
$kind = $argument->kind;
if ($kind === ast\AST_CLOSURE) {
if (Config::get_track_references()) {
$this->trackReferenceToClosure($argument);
}
}
// If the parameter is pass-by-reference and we're
// passing a variable in, see if we should pass
// the parameter and variable types to each other
if ($parameter->isPassByReference()) {
self::analyzePassByReferenceArgument(
$code_base,
$context,
$argument,
$argument_list,
$method,
$parameter,
$method->getRealParameterForCaller($i),
$i
);
}
}
// If we're in quick mode, don't retest methods based on
// parameter types passed in
if (Config::get_quick_mode()) {
return;
}
// Don't re-analyze recursive methods. That doesn't go
// well.
if ($context->isInFunctionLikeScope()
&& $method->getFQSEN() === $context->getFunctionLikeFQSEN()
) {
$this->checkForInfiniteRecursion($node, $method);
return;
}
if (!$method->needsRecursiveAnalysis()) {
return;
}
// Re-analyze the method with the types of the arguments
// being passed in.
$this->analyzeMethodWithArgumentTypes(
$node->children['args'],
$method
);
}
/**
* @param Parameter $parameter the parameter types inferred from combination of real and union type
*
* @param ?Parameter $real_parameter the real parameter type from the type signature
*/
private function createPassByReferenceArgumentInCall(FunctionInterface $method, Node $argument, Parameter $parameter, ?Parameter $real_parameter): void
{
if ($argument->kind === ast\AST_VAR) {
// We don't do anything with the new variable; just create it
// if it doesn't exist
try {
$variable = (new ContextNode(
$this->code_base,
$this->context,
$argument
))->getOrCreateVariableForReferenceParameter($parameter, $real_parameter);
$variable_union_type = $variable->getUnionType();
if ($variable_union_type->hasRealTypeSet()) {
// TODO: Do a better job handling the large number of edge cases
// - e.g. infer that stream_select will convert non-empty arrays to possibly empty arrays, while the result continues to have a real type of array.
if ($method->getContext()->isPHPInternal() && \in_array($parameter->getReferenceType(), [Parameter::REFERENCE_IGNORED, Parameter::REFERENCE_READ_WRITE], true)) {
if (\preg_match('/shuffle|sort|array_(unshift|shift|push|pop|splice)/i', $method->getName())) {
// This use case is probably handled by MiscParamPlugin
return;
}
}
$variable->setUnionType($variable->getUnionType()->eraseRealTypeSetRecursively());
}
} catch (NodeException $_) {
return;
}
} elseif ($argument->kind === ast\AST_STATIC_PROP
|| $argument->kind === ast\AST_PROP
) {
$property_name = $argument->children['prop'];
if ($property_name instanceof Node) {
$property_name = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $property_name)->asSingleScalarValueOrNullOrSelf();
}
// Only try to handle known literals or strings, ignore properties with names that couldn't be inferred.
if (\is_string($property_name)) {
// We don't do anything with it; just create it
// if it doesn't exist
try {
$property = (new ContextNode(
$this->code_base,
$this->context,
$argument
))->getOrCreateProperty($property_name, $argument->kind === ast\AST_STATIC_PROP);
$property->setHasWriteReference();
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// If we can't figure out what kind of a call
// this is, don't worry about it
}
}
}
}
/**
* @param list<Node|string|int|float> $argument_list the arguments of the invocation, containing the pass by reference argument
*
* @param Parameter $parameter the parameter types inferred from combination of real and union type
*
* @param ?Parameter $real_parameter the real parameter type from the type signature
*/
private static function analyzePassByReferenceArgument(
CodeBase $code_base,
Context $context,
Node $argument,
array $argument_list,
FunctionInterface $method,
Parameter $parameter,
?Parameter $real_parameter,
int $parameter_offset
): void {
$variable = null;
$kind = $argument->kind;
if ($kind === ast\AST_VAR) {
try {
$variable = (new ContextNode(
$code_base,
$context,
$argument
))->getOrCreateVariableForReferenceParameter($parameter, $real_parameter);
} catch (NodeException $_) {
// E.g. `function_accepting_reference(${$varName})` - Phan can't analyze outer type of ${$varName}
return;
}
} elseif ($kind === ast\AST_STATIC_PROP
|| $kind === ast\AST_PROP
) {
$property_name = $argument->children['prop'];
if ($property_name instanceof Node) {
$property_name = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $property_name)->asSingleScalarValueOrNullOrSelf();
}
// Only try to handle property names that could be inferred.
if (\is_string($property_name)) {
// We don't do anything with it; just create it
// if it doesn't exist
try {
$variable = (new ContextNode(
$code_base,
$context,
$argument
))->getOrCreateProperty($property_name, $argument->kind === ast\AST_STATIC_PROP);
$variable->addReference($context);
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$code_base,
$context,
$exception->getIssueInstance()
);
} catch (Exception $_) {
// If we can't figure out what kind of a call
// this is, don't worry about it
}
}
}
if ($variable) {
$set_variable_type = static function (UnionType $new_type) use ($code_base, $context, $variable, $argument): void {
if ($variable instanceof Variable) {
$variable = clone($variable);
AssignmentVisitor::analyzeSetUnionTypeInContext($code_base, $context, $variable, $new_type, $argument);
$context->addScopeVariable($variable);
} else {
// This is a Property. Add any compatible new types to the type of the property.
AssignmentVisitor::addTypesToPropertyStandalone($code_base, $context, $variable, $new_type);
}
};
if ($variable instanceof Property) {
// TODO: If @param-out is ever supported, then use that type to check
self::checkPassingPropertyByReference($code_base, $context, $method, $parameter, $argument, $variable, $parameter_offset);
}
switch ($parameter->getReferenceType()) {
case Parameter::REFERENCE_WRITE_ONLY:
self::analyzeWriteOnlyReference($code_base, $context, $method, $set_variable_type, $argument_list, $parameter);
break;
case Parameter::REFERENCE_READ_WRITE:
$reference_parameter_type = $parameter->getNonVariadicUnionType();
$variable_type = $variable->getUnionType();
if ($variable_type->isEmpty()) {
// if Phan doesn't know the variable type,
// then guess that the variable is the type of the reference
// when analyzing the following statements.
$set_variable_type($reference_parameter_type);
} elseif (!$variable_type->canCastToUnionType($reference_parameter_type)) {
// Phan already warned about incompatible types.
// But analyze the following statements as if it could have been the type expected,
// to reduce false positives.
$set_variable_type($variable->getUnionType()->withUnionType(
$reference_parameter_type
));
}
// don't modify - assume the function takes the same type in that it returns,
// and we want to preserve generic array types for sorting functions (May change later on)
// TODO: Check type compatibility earlier, and don't modify?
break;
case Parameter::REFERENCE_IGNORED:
// Pretend this reference doesn't modify the passed in argument.
break;
case Parameter::REFERENCE_DEFAULT:
default:
$reference_parameter_type = $parameter->getNonVariadicUnionType();
// We have no idea what type of reference this is.
// Probably user defined code.
$set_variable_type($variable->getUnionType()->withUnionType(
$reference_parameter_type
));
break;
}
}
}
/**
* @param Closure(UnionType):void $set_variable_type
* @param list<Node|string|int|float> $argument_list
*/
private static function analyzeWriteOnlyReference(
CodeBase $code_base,
Context $context,
FunctionInterface $method,
Closure $set_variable_type,
array $argument_list,
Parameter $parameter
): void {
switch ($method->getFQSEN()->__toString()) {
case '\preg_match':
$set_variable_type(
RegexAnalyzer::getPregMatchUnionType($code_base, $context, $argument_list)
);
return;
case '\preg_match_all':
$set_variable_type(
RegexAnalyzer::getPregMatchAllUnionType($code_base, $context, $argument_list)
);
return;
default:
$reference_parameter_type = $parameter->getNonVariadicUnionType();
// The previous value is being ignored, and being replaced.
// FIXME: Do something different for properties, e.g. limit it to a scope, combine with old property, etc.
$set_variable_type(
$reference_parameter_type
);
}
}
private function trackReferenceToClosure(Node $argument): void
{
try {
$inner_context = $this->context->withLineNumberStart($argument->lineno);
$method = (new ContextNode(
$this->code_base,
$inner_context,
$argument
))->getClosure();
$method->addReference($inner_context);
} catch (Exception $_) {
// Swallow it
}
}
/**
* Replace the method's parameter types with the argument
* types and re-analyze the method.
*
* This is used when analyzing callbacks and closures, e.g. in array_map.
*
* @param list<UnionType> $argument_types
* An AST node listing the arguments
*
* @param FunctionInterface $method
* The method or function being called
* @see analyzeMethodWithArgumentTypes (Which takes AST nodes)
*
* @param list<Node|mixed> $arguments
* An array of arguments to the callable, to analyze references.
*
* @param bool $erase_old_return_type
* Whether $method's old return type should be erased
* to use the newly inferred type based on $argument_types.
* (useful for array_map, etc)
*/
public function analyzeCallableWithArgumentTypes(
array $argument_types,
FunctionInterface $method,
array $arguments = [],
bool $erase_old_return_type = false
): void {
$method = $this->findDefiningMethod($method);
if (!$method->needsRecursiveAnalysis()) {
return;
}
// Don't re-analyze recursive methods. That doesn't go well.
if ($this->context->isInFunctionLikeScope()
&& $method->getFQSEN() === $this->context->getFunctionLikeFQSEN()
) {
return;
}
foreach ($argument_types as $i => $type) {
$argument_types[$i] = $type->withStaticResolvedInContext($this->context);
}
$original_method_scope = $method->getInternalScope();
$method->setInternalScope(clone($original_method_scope));
try {
// Even though we don't modify the parameter list, we still need to know the types
// -- as an optimization, we don't run quick mode again if the types didn't change?
$parameter_list = \array_map(static function (Parameter $parameter): Parameter {
return clone($parameter);
}, $method->getParameterList());
foreach ($parameter_list as $i => $parameter_clone) {
if (!isset($argument_types[$i]) && $parameter_clone->hasDefaultValue()) {
$parameter_type = $parameter_clone->getDefaultValueType()->withRealTypeSet($parameter_clone->getNonVariadicUnionType()->getRealTypeSet());
if ($parameter_type->isType(NullType::instance(false))) {
// Treat a parameter default of null the same way as passing null to that parameter
// (Add null to the list of possibilities)
$parameter_clone->addUnionType($parameter_type);
} else {
// For other types (E.g. string), just replace the union type.
$parameter_clone->setUnionType($parameter_type);
}
}
// Add the parameter to the scope
$method->getInternalScope()->addVariable(
$parameter_clone->asNonVariadic()
);
// If there's no parameter at that offset, we may be in
// a ParamTooMany situation. That is caught elsewhere.
if (!isset($argument_types[$i])
|| !$parameter_clone->hasEmptyNonVariadicType()
) {
continue;
}
$this->updateParameterTypeByArgument(
$method,
$parameter_clone,
$arguments[$i] ?? null,
$argument_types,
$parameter_list,
$i
);
}
foreach ($parameter_list as $parameter_clone) {
if ($parameter_clone->isVariadic()) {
// We're using this parameter clone to analyze the **inside** of the method, it's never seen on the outside.
// Convert it immediately.
// TODO: Add tests of variadic references, fix those if necessary.
$method->getInternalScope()->addVariable(
$parameter_clone->cloneAsNonVariadic()
);
}
}
// Now that we know something about the parameters used
// to call the method, we can reanalyze the method with
// the types of the parameter
if ($erase_old_return_type) {
$method->setUnionType($method->getOriginalReturnType());
}
$method->analyzeWithNewParams($method->getContext(), $this->code_base, $parameter_list);
} finally {
$method->setInternalScope($original_method_scope);
}
}
/**
* Replace the method's parameter types with the argument
* types and re-analyze the method.
*
* @param Node $argument_list_node
* An AST node listing the arguments
*
* @param FunctionInterface $method
* The method or function being called
* Precondition: $method->needsRecursiveAnalysis() === false
*
* @return void
*
* TODO: deduplicate code.
*/
private function analyzeMethodWithArgumentTypes(
Node $argument_list_node,
FunctionInterface $method
): void {
$method = $this->findDefiningMethod($method);
$original_method_scope = $method->getInternalScope();
$method->setInternalScope(clone($original_method_scope));
$method_context = $method->getContext();
try {
// Even though we don't modify the parameter list, we still need to know the types
// -- as an optimization, we don't run quick mode again if the types didn't change?
$parameter_list = \array_map(static function (Parameter $parameter): Parameter {
return $parameter->cloneAsNonVariadic();
}, $method->getParameterList());
// always resolve all arguments outside of quick mode to detect undefined variables, other problems in call arguments.
// Fixes https://github.com/phan/phan/issues/583
$argument_types = [];
foreach ($argument_list_node->children as $i => $argument) {
// Determine the type of the argument at position $i
$argument_types[$i] = UnionTypeVisitor::unionTypeFromNode(
$this->code_base,
$this->context,
$argument,
true
)->withStaticResolvedInContext($this->context)->eraseRealTypeSetRecursively();
}
foreach ($parameter_list as $i => $parameter_clone) {
$argument = $argument_list_node->children[$i] ?? null;
if ($argument === null
&& $parameter_clone->hasDefaultValue()
) {
$parameter_type = $parameter_clone->getDefaultValueType()->withRealTypeSet($parameter_clone->getNonVariadicUnionType()->getRealTypeSet());
if ($parameter_type->isType(NullType::instance(false))) {
// Treat a parameter default of null the same way as passing null to that parameter
// (Add null to the list of possibilities)
$parameter_clone->addUnionType($parameter_type);
} else {
// For other types (E.g. string), just replace the union type.
$parameter_clone->setUnionType($parameter_type);
}
}
// Add the parameter to the scope
// TODO: asNonVariadic()?
$method->getInternalScope()->addVariable(
$parameter_clone
);
// If there's no parameter at that offset, we may be in
// a ParamTooMany situation. That is caught elsewhere.
if ($argument === null) {
continue;
}
// If there's a declared type for the parameter,
// then don't bother overriding the type to analyze the function/method body (unless the parameter is pass-by-reference)
// Note that $parameter_clone was converted to a non-variadic clone, so the getNonVariadicUnionType returns an array.
if (!$parameter_clone->hasEmptyNonVariadicType() && !$parameter_clone->isPassByReference()) {
continue;
}
$this->updateParameterTypeByArgument(
$method,
$parameter_clone,
$argument,
$argument_types,
$parameter_list,
$i
);
}
foreach ($parameter_list as $parameter_clone) {
if ($parameter_clone->isVariadic()) {
// We're using this parameter clone to analyze the **inside** of the method, it's never seen on the outside.
// Convert it immediately.
// TODO: Add tests of variadic references, fix those if necessary.
$method->getInternalScope()->addVariable(
$parameter_clone->cloneAsNonVariadic()
);
}
}
// Now that we know something about the parameters used
// to call the method, we can reanalyze the method with
// the types of the parameter
$method->analyzeWithNewParams($method_context, $this->code_base, $parameter_list);
} finally {
$method->setInternalScope($original_method_scope);
}
}
private function findDefiningMethod(FunctionInterface $method): FunctionInterface
{
if ($method instanceof Method) {
$defining_fqsen = $method->getDefiningFQSEN();
if ($method->getFQSEN() !== $defining_fqsen) {
// This should always happen, unless in the language server mode
if ($this->code_base->hasMethodWithFQSEN($defining_fqsen)) {
return $this->code_base->getMethodByFQSEN($defining_fqsen);
}
}
}
return $method;
}
/**
* Check if $argument_list_node calling itself is likely to be a case of infinite recursion.
* This is based on heuristics, and will not catch all cases.
*/
private function checkForInfiniteRecursion(Node $node, FunctionInterface $method): void
{
$argument_list_node = $node->children['args'];
$kind = $node->kind;
if ($kind === ast\AST_METHOD_CALL || $kind === ast\AST_NULLSAFE_METHOD_CALL) {
$expr = $node->children['expr'];
if (!$expr instanceof Node || $expr->kind !== ast\AST_VAR || $expr->children['name'] !== 'this') {
return;
}
}
$nearest_function_like = null;
foreach ($this->parent_node_list as $c) {
if (\in_array($c->kind, [ast\AST_FUNC_DECL, ast\AST_METHOD, ast\AST_CLOSURE], true)) {
$nearest_function_like = $c;
}
}
if (!$nearest_function_like) {
return;
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable this is never null
if (ReachabilityChecker::willUnconditionallyBeReached($nearest_function_like->children['stmts'], $argument_list_node)) {
$this->emitIssue(
Issue::InfiniteRecursion,
$node->lineno,
$method->getNameForIssue()
);
return;
}
$this->checkForInfiniteRecursionWithSameArgs($node, $method);
}
private function checkForInfiniteRecursionWithSameArgs(Node $node, FunctionInterface $method): void
{
$argument_list_node = $node->children['args'];
$parameter_list = $method->getParameterList();
if (\count($argument_list_node->children) !== \count($parameter_list)) {
return;
}
if (\count($argument_list_node->children) === 0) {
$this->emitIssue(
Issue::PossibleInfiniteRecursionSameParams,
$node->lineno,
$method->getNameForIssue()
);
return;
}
// TODO also check AST_UNPACK against variadic
$arg_names = [];
foreach ($argument_list_node->children as $i => $arg) {
if (!$arg instanceof Node) {
return;
}
$is_unpack = false;
if ($arg->kind === ast\AST_UNPACK) {
$arg = $arg->children['expr'];
if (!$arg instanceof Node) {
return;
}
$is_unpack = true;
}
if ($arg->kind !== ast\AST_VAR) {
return;
}
$arg_name = $arg->children['name'];
if (!\is_string($arg_name)) {
return;
}
$param = $parameter_list[$i];
if ($param->getName() !== $arg_name || $param->isVariadic() !== $is_unpack) {
return;
}
$arg_names[] = $arg_name;
}
$outer_scope = $method->getInternalScope();
$current_scope = $this->context->getScope();
foreach ($arg_names as $arg_name) {
if (!$current_scope->hasVariableWithName($arg_name) || !$outer_scope->hasVariableWithName($arg_name)) {
return;
}
}
// @phan-suppress-next-line PhanUndeclaredProperty
$node->check_infinite_recursion = [$arg_names, $method->getNameForIssue()];
}
/**
* @param FunctionInterface $method
* The method that we're updating parameter types for
*
* @param Parameter $parameter
* The parameter that we're updating
*
* @param Node|mixed $argument
* The argument whose type we'd like to replace the
* parameter type with.
*
* @param list<UnionType> $argument_types
* The type of arguments
*
* @param list<Parameter> &$parameter_list
* The parameter list - types are modified by reference
*
* @param int $parameter_offset
* The offset of the parameter on the method's
* signature.
*/
private function updateParameterTypeByArgument(
FunctionInterface $method,
Parameter $parameter,
$argument,
array $argument_types,
array &$parameter_list,
int $parameter_offset
): void {
$argument_type = $argument_types[$parameter_offset];
if ($parameter->isVariadic()) {
for ($i = $parameter_offset + 1; $i < \count($argument_types); $i++) {
$argument_type = $argument_type->withUnionType($argument_types[$i]);
}
}
// $argument_type = $this->filterValidArgumentTypes($argument_type, $non_variadic_parameter_type);
if (!$argument_type->isEmpty()) {
// Then set the new type on that parameter based
// on the argument's type. We'll use this to
// retest the method with the passed in types
// TODO: if $argument_type is non-empty and !isType(NullType), instead use setUnionType?
if ($parameter->isCloneOfVariadic()) {
// For https://github.com/phan/phan/issues/1525 : Collapse array shapes into generic arrays before recursively analyzing a method.
if ($parameter->hasEmptyNonVariadicType()) {
$parameter->setUnionType(
$argument_type->withFlattenedArrayShapeOrLiteralTypeInstances()->asListTypes()->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet())
);
} else {
$parameter->addUnionType(
$argument_type->withFlattenedArrayShapeOrLiteralTypeInstances()->asListTypes()->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet())
);
}
} else {
$parameter->addUnionType(
($method instanceof Func && $method->isClosure() ? $argument_type : $argument_type->withFlattenedArrayShapeOrLiteralTypeInstances())->withRealTypeSet($parameter->getNonVariadicUnionType()->getRealTypeSet())
);
}
if ($method instanceof Method && ($parameter->getFlags() & Parameter::PARAM_MODIFIER_VISIBILITY_FLAGS)) {
$this->analyzeArgumentWithConstructorPropertyPromotion($method, $parameter);
}
}
// If we're passing by reference, get the variable
// we're dealing with wrapped up and shoved into
// the scope of the method
if (!$parameter->isPassByReference()) {
// Overwrite the method's variable representation
// of the parameter with the parameter with the
// new type
$method->getInternalScope()->addVariable(
$parameter
);
return;
}
// At this point we're dealing with a pass-by-reference
// parameter.
// For now, give up and work on it later.
//
// TODO (Issue #376): It's possible to have a
// parameter `&...$args`. Analyzing that is going to
// be a problem. Is it possible to create
// `PassByReferenceVariableCollection extends Variable`
// or something similar?
if ($parameter->isVariadic()) {
return;
}
if (!$argument instanceof Node) {
return;
}
$variable = null;
if ($argument->kind === ast\AST_VAR) {
try {
$variable = (new ContextNode(
$this->code_base,
$this->context,
$argument
))->getOrCreateVariableForReferenceParameter($parameter, $method->getRealParameterForCaller($parameter_offset));
} catch (NodeException $_) {
// Could not figure out the node name
return;
}
} elseif (\in_array($argument->kind, [ast\AST_STATIC_PROP, ast\AST_PROP], true)) {
try {
$variable = (new ContextNode(
$this->code_base,
$this->context,
$argument
))->getProperty($argument->kind === ast\AST_STATIC_PROP);
} catch (IssueException | NodeException $_) {
// Hopefully caught elsewhere
}
}
// If we couldn't find a variable, give up
if (!$variable) {
return;
}
// For @phan-ignore-reference, don't bother modifying the type
if ($parameter->getReferenceType() === Parameter::REFERENCE_IGNORED) {
return;
}
$pass_by_reference_variable =
new PassByReferenceVariable(
$parameter,
$variable,
$this->code_base,
$this->context
);
// Add it to the (cloned) scope of the function wrapped
// in a way that makes it addressable as the
// parameter its mimicking
$method->getInternalScope()->addVariable(
$pass_by_reference_variable
);
$parameter_list[$parameter_offset] = $pass_by_reference_variable;
}
private function analyzeArgumentWithConstructorPropertyPromotion(Method $method, Parameter $parameter): void
{
if (!$method->isNewConstructor()) {
return;
}
$code_base = $this->code_base;
$class_fqsen = $method->getClassFQSEN();
$class = $code_base->getClassByFQSEN($class_fqsen);
$property = $class->getPropertyByName($code_base, $parameter->getName());
AssignmentVisitor::addTypesToPropertyStandalone($code_base, $this->context, $property, $parameter->getUnionType());
}
/**
* Emit warnings if the pass-by-reference call would set the property to an invalid type
* @param Node $argument a node of kind ast\AST_PROP or ast\AST_STATIC_PROP
*/
private static function checkPassingPropertyByReference(CodeBase $code_base, Context $context, FunctionInterface $method, Parameter $parameter, Node $argument, Property $property, int $parameter_offset): void
{
$parameter_type = $parameter->getNonVariadicUnionType();
$expr_node = $argument->children['expr'] ?? null;
if ($expr_node instanceof Node &&
$expr_node->kind === ast\AST_VAR &&
$expr_node->children['name'] === 'this') {
// If the property is of the form $this->prop, check for local assignments and conditions on $this->prop
$property_type = UnionTypeVisitor::unionTypeFromNode($code_base, $context, $argument);
} else {
$property_type = $property->getUnionType();
}
if ($property_type->hasRealTypeSet()) {
// Barely any reference parameters will have real union types (and phan would already warn about passing them in if they did),
// so warn if the phpdoc type doesn't match the property's real type.
if (!$parameter_type->canCastToDeclaredType($code_base, $context, $property_type)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeMismatchArgumentPropertyReferenceReal,
$argument->lineno,
$parameter_offset,
$property->getRepresentationForIssue(),
$property_type,
self::toDetailsForRealTypeMismatch($property_type),
$method->getRepresentationForIssue(),
$parameter_type,
self::toDetailsForRealTypeMismatch($parameter_type)
);
return;
}
}
if ($parameter_type->canCastToDeclaredType($code_base, $context, $property_type)) {
return;
}
Issue::maybeEmit(
$code_base,
$context,
Issue::TypeMismatchArgumentPropertyReference,
$argument->lineno,
$parameter_offset,
$property->getRepresentationForIssue(),
$property_type,
$method->getRepresentationForIssue(),
$parameter_type
);
}
private function isInNoOpPosition(Node $node): bool
{
$parent_node = \end($this->parent_node_list);
if (!($parent_node instanceof Node)) {
return false;
}
switch ($parent_node->kind) {
case ast\AST_STMT_LIST:
return true;
case ast\AST_EXPR_LIST:
$parent_parent_node = \prev($this->parent_node_list);
// @phan-suppress-next-line PhanPossiblyUndeclaredProperty
if ($parent_parent_node->kind === ast\AST_MATCH_ARM) {
return false;
}
if ($node !== \end($parent_node->children)) {
return true;
}
// This is an expression list, but it's in the condition
return $parent_node !== ($parent_parent_node->children['cond'] ?? null);
}
return false;
}
/**
* @param Node $node
* A node to check to see if it's a no-op
*
* @param string $issue_type
* A message to emit if it's a no-op
*/
private function analyzeNoOp(Node $node, string $issue_type): void
{
if ($this->isInNoOpPosition($node)) {
$this->emitIssue(
$issue_type,
$node->lineno
);
}
}
private static function hasEmptyImplementation(Method $method): bool
{
if ($method->isAbstract() || $method->isPHPInternal()) {
return false;
}
$stmts = $method->getNode()->children['stmts'] ?? null;
if (!$stmts instanceof Node) {
// This is abstract or a stub or a magic method
return false;
}
return empty($stmts->children);
}
/**
* @param list<Clazz> $class_list
*/
private function warnNoopNew(
Node $node,
array $class_list
): void {
$has_constructor_or_destructor = \count($class_list) === 0;
foreach ($class_list as $class) {
if ($class->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_CONSTRUCTOR_USED_FOR_SIDE_EFFECTS)) {
return;
}
}
foreach ($class_list as $class) {
if ($class->hasMethodWithName($this->code_base, '__construct', true)) {
$constructor = $class->getMethodByName($this->code_base, '__construct');
if (!$constructor->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_FAKE_CONSTRUCTOR)) {
if (!self::hasEmptyImplementation($constructor)) {
$has_constructor_or_destructor = true;
break;
}
}
}
if ($class->hasMethodWithName($this->code_base, '__destruct', true)) {
$destructor = $class->getMethodByName($this->code_base, '__destruct');
if (!self::hasEmptyImplementation($destructor)) {
$has_constructor_or_destructor = true;
break;
}
}
if (!$class->isClass() || $class->isAbstract()) {
$has_constructor_or_destructor = true;
break;
}
}
$this->emitIssue(
$has_constructor_or_destructor ? Issue::NoopNew : Issue::NoopNewNoSideEffects,
$node->lineno,
ASTReverter::toShortString($node)
);
}
public const LOOP_SCOPE_KINDS = [
ast\AST_FOR => true,
ast\AST_FOREACH => true,
ast\AST_WHILE => true,
ast\AST_DO_WHILE => true,
ast\AST_SWITCH => true,
];
/**
* Analyzes a `break;` or `break N;` statement.
* Checks if there are enough loops to break out of.
*/
public function visitBreak(Node $node): Context
{
$depth = $node->children['depth'] ?? 1;
if (!\is_int($depth)) {
return $this->context;
}
foreach ($this->parent_node_list as $iter_node) {
if (\array_key_exists($iter_node->kind, self::LOOP_SCOPE_KINDS)) {
$depth--;
if ($depth <= 0) {
return $this->context;
}
}
}
$this->warnBreakOrContinueWithoutLoop($node);
return $this->context;
}
/**
* Analyzes a `continue;` or `continue N;` statement.
* Checks for http://php.net/manual/en/migration73.incompatible.php#migration73.incompatible.core.continue-targeting-switch
* and similar issues.
*/
public function visitContinue(Node $node): Context
{
$nodes = $this->parent_node_list;
$depth = $node->children['depth'] ?? 1;
if (!\is_int($depth)) {
return $this->context;
}
for ($iter_node = \end($nodes); $iter_node instanceof Node; $iter_node = \prev($nodes)) {
switch ($iter_node->kind) {
case ast\AST_FOR:
case ast\AST_FOREACH:
case ast\AST_WHILE:
case ast\AST_DO_WHILE:
$depth--;
if ($depth <= 0) {
return $this->context;
}
break;
case ast\AST_SWITCH:
$depth--;
if ($depth <= 0) {
$this->emitIssue(
Issue::ContinueTargetingSwitch,
$node->lineno
);
return $this->context;
}
break;
}
}
$this->warnBreakOrContinueWithoutLoop($node);
return $this->context;
}
/**
* Visit a node of kind AST_LABEL to check for unused labels.
* @override
*/
public function visitLabel(Node $node): Context
{
$label = $node->children['name'];
$used_labels = GotoAnalyzer::getLabelSet($this->parent_node_list);
if (!isset($used_labels[$label])) {
$this->emitIssue(
Issue::UnusedGotoLabel,
$node->lineno,
$label
);
}
return $this->context;
}
private function warnBreakOrContinueWithoutLoop(Node $node): void
{
$depth = $node->children['depth'] ?? 1;
$name = $node->kind === ast\AST_BREAK ? 'break' : 'continue';
if ($depth !== 1) {
$this->emitIssue(
Issue::ContinueOrBreakTooManyLevels,
$node->lineno,
$name,
$depth
);
return;
}
$this->emitIssue(
Issue::ContinueOrBreakNotInLoop,
$node->lineno,
$name
);
}
/**
* @param Node $node
* A decl to check to see if its only effect
* is the throw an exception
*
* @return bool
* True when the decl can only throw an exception or return or exit()
*/
private static function declOnlyThrows(Node $node): bool
{
// Work around fallback parser generating methods without statements list.
// Otherwise, 'stmts' would always be a Node due to preconditions.
$stmts_node = $node->children['stmts'];
return $stmts_node instanceof Node && BlockExitStatusChecker::willUnconditionallyThrowOrReturn($stmts_node);
}
/**
* Check if the class is using PHP4-style constructor (without having its own __construct method)
*
* @param Clazz $class
* @param Method $method
*/
private function checkForPHP4StyleConstructor(Clazz $class, Method $method): void
{
if ($class->isClass()
&& ($class->getElementNamespace() ?: "\\") === "\\"
&& \strcasecmp($class->getName(), $method->getName()) === 0
&& $class->hasMethodWithName($this->code_base, "__construct", false) // return true for the fake constructor
) {
try {
$constructor = $class->getMethodByName($this->code_base, "__construct");
// Phan always makes up the __construct if it's not explicitly defined, so we need to check
// if there is no __construct method *actually* defined before we emit the issue
if ($constructor->getPhanFlagsHasState(\Phan\Language\Element\Flags::IS_FAKE_CONSTRUCTOR)) {
Issue::maybeEmit(
$this->code_base,
$this->context,
Issue::CompatiblePHP8PHP4Constructor,
$this->context->getLineNumberStart(),
$method->getRepresentationForIssue()
);
}
} catch (CodeBaseException $_) {
// actually __construct always exists as per Phan's current logic, so this exception won't be thrown.
// but just in case let's leave this here
}
}
}
/**
* @param Node $node @unused-param
* A node to analyze
*
* @return Context
* A new or an unchanged context resulting from
* parsing the node
*/
public function visitThrow(Node $node): Context
{
$parent_node = \end($this->parent_node_list);
if (!($parent_node instanceof Node)) {
return $this->context;
}
if ($parent_node->kind !== ast\AST_STMT_LIST) {
if (Config::get_closest_minimum_target_php_version_id() < 80000) {
$this->emitIssue(
Issue::CompatibleThrowExpression,
$parent_node->lineno,
ASTReverter::toShortString($parent_node)
);
}
}
return $this->context;
}
}