Skip to content
11 changes: 10 additions & 1 deletion src/Analyser/ExprHandler/AssignOpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand All @@ -22,6 +23,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use function array_merge;
use function get_class;
use function sprintf;

Expand All @@ -35,6 +37,7 @@ final class AssignOpHandler implements ExprHandler
public function __construct(
private AssignHandler $assignHandler,
private InitializerExprTypeResolver $initializerExprTypeResolver,
private ImplicitToStringCallHelper $implicitToStringCallHelper,
)
{
}
Expand Down Expand Up @@ -85,19 +88,25 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex
}
$scope = $assignResult->getScope();
$throwPoints = $assignResult->getThrowPoints();
$impurePoints = $assignResult->getImpurePoints();
if (
($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) &&
!$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
) {
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
}
if ($expr instanceof Expr\AssignOp\Concat) {
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
}

return new ExpressionResult(
$scope,
hasYield: $assignResult->hasYield(),
isAlwaysTerminating: $assignResult->isAlwaysTerminating(),
throwPoints: $throwPoints,
impurePoints: $assignResult->getImpurePoints(),
impurePoints: $impurePoints,
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
);
Expand Down
11 changes: 10 additions & 1 deletion src/Analyser/ExprHandler/BinaryOpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private RicherScopeGetTypeHelper $richerScopeGetTypeHelper,
private PhpVersion $phpVersion,
private ImplicitToStringCallHelper $implicitToStringCallHelper,
)
{
}
Expand All @@ -62,20 +64,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftResult->getScope(), $storage, $nodeCallback, $context->enterDeep());
$throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints());
$impurePoints = array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints());
if (
($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
!$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
) {
$throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false);
}
if ($expr instanceof BinaryOp\Concat) {
$leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->left, $scope);
$rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->right, $leftResult->getScope());
$throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints());
}
$scope = $rightResult->getScope();

return new ExpressionResult(
$scope,
hasYield: $leftResult->hasYield() || $rightResult->hasYield(),
isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(),
throwPoints: $throwPoints,
impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
impurePoints: $impurePoints,
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
);
Expand Down
38 changes: 6 additions & 32 deletions src/Analyser/ExprHandler/CastStringHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\ExpressionContext;
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Type;
use function sprintf;
use function array_merge;

/**
* @implements ExprHandler<Cast\String_>
Expand All @@ -29,8 +26,7 @@ final class CastStringHandler implements ExprHandler

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private PhpVersion $phpVersion,
private MethodThrowPointHelper $methodThrowPointHelper,
private ImplicitToStringCallHelper $implicitToStringCallHelper,
)
{
}
Expand All @@ -46,31 +42,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$impurePoints = $exprResult->getImpurePoints();
$throwPoints = $exprResult->getThrowPoints();

$exprType = $scope->getType($expr->expr);
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
if ($toStringMethod !== null) {
if (!$toStringMethod->hasSideEffects()->no()) {
$impurePoints[] = new ImpurePoint(
$scope,
$expr,
'methodCall',
sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
$toStringMethod->isPure()->no(),
);
}

if ($this->phpVersion->throwsOnStringCast()) {
$throwPoint = $this->methodThrowPointHelper->getThrowPoint(
$toStringMethod,
$toStringMethod->getOnlyVariant(),
new Expr\MethodCall($expr->expr, new Identifier('__toString')),
$scope,
);
if ($throwPoint !== null) {
$throwPoints[] = $throwPoint;
}
}
}
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());

$scope = $exprResult->getScope();

Expand Down
73 changes: 73 additions & 0 deletions src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use function sprintf;

#[AutowiredService]
final class ImplicitToStringCallHelper
{

public function __construct(
private PhpVersion $phpVersion,
private MethodThrowPointHelper $methodThrowPointHelper,
)
{
}

public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): ExpressionResult
{
$throwPoints = [];
$impurePoints = [];

$exprType = $scope->getType($expr);
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
if ($toStringMethod === null) {
return new ExpressionResult(
$scope,
hasYield: false,
isAlwaysTerminating: false,
throwPoints: [],
impurePoints: [],
);
}

if (!$toStringMethod->hasSideEffects()->no()) {
$impurePoints[] = new ImpurePoint(
$scope,
$expr,
'methodCall',
sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
$toStringMethod->isPure()->no(),
);
}

if ($this->phpVersion->throwsOnStringCast()) {
$throwPoint = $this->methodThrowPointHelper->getThrowPoint(
$toStringMethod,
$toStringMethod->getOnlyVariant(),
new Expr\MethodCall($expr, new Identifier('__toString')),
$scope,
);
if ($throwPoint !== null) {
$throwPoints[] = $throwPoint;
}
}

return new ExpressionResult(
$scope,
hasYield: false,
isAlwaysTerminating: false,
throwPoints: $throwPoints,
impurePoints: $impurePoints,
);
}

}
24 changes: 23 additions & 1 deletion src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
Expand Down Expand Up @@ -52,7 +53,9 @@ public function methodCallReturnType(
}

$resolvedTypes = [];
foreach ($typeWithMethod->getObjectClassNames() as $className) {
$allClassNames = $typeWithMethod->getObjectClassNames();
$handledClassNames = [];
foreach ($allClassNames as $className) {
if ($normalizedMethodCall instanceof MethodCall) {
foreach ($this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
Expand All @@ -65,6 +68,7 @@ public function methodCallReturnType(
}

$resolvedTypes[] = $resolvedType;
$handledClassNames[] = $className;
}
} else {
foreach ($this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) {
Expand All @@ -82,11 +86,29 @@ public function methodCallReturnType(
}

$resolvedTypes[] = $resolvedType;
$handledClassNames[] = $className;
}
}
}

if (count($resolvedTypes) > 0) {
if (count($allClassNames) !== count($handledClassNames)) {
$remainingType = $typeWithMethod;
foreach ($handledClassNames as $handledClassName) {
$remainingType = TypeCombinator::remove($remainingType, new ObjectType($handledClassName));
}
if ($remainingType->hasMethod($methodName)->yes()) {
$remainingMethod = $remainingType->getMethod($methodName, $scope);
$remainingParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$remainingMethod->getVariants(),
$remainingMethod->getNamedArgumentsVariants(),
);
$resolvedTypes[] = $remainingParametersAcceptor->getReturnType();
}
}

return VoidToNullTypeTransformer::transform(TypeCombinator::union(...$resolvedTypes), $methodCall);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Analyser/ExprHandler/InterpolatedStringHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredService;
Expand All @@ -27,6 +28,7 @@ final class InterpolatedStringHandler implements ExprHandler

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private ImplicitToStringCallHelper $implicitToStringCallHelper,
)
{
}
Expand All @@ -50,6 +52,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$hasYield = $hasYield || $partResult->hasYield();
$throwPoints = array_merge($throwPoints, $partResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $partResult->getImpurePoints());

$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope);
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());

$isAlwaysTerminating = $isAlwaysTerminating || $partResult->isAlwaysTerminating();
$scope = $partResult->getScope();
}
Expand Down
18 changes: 16 additions & 2 deletions src/Analyser/ExprHandler/PrintHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand All @@ -24,6 +25,12 @@
final class PrintHandler implements ExprHandler
{

public function __construct(
private ImplicitToStringCallHelper $implicitToStringCallHelper,
)
{
}

public function supports(Expr $expr): bool
{
return $expr instanceof Print_;
Expand All @@ -37,14 +44,21 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
{
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
$throwPoints = $exprResult->getThrowPoints();
$impurePoints = $exprResult->getImpurePoints();

$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());

$scope = $exprResult->getScope();

return new ExpressionResult(
$scope,
hasYield: $exprResult->hasYield(),
isAlwaysTerminating: $exprResult->isAlwaysTerminating(),
throwPoints: $exprResult->getThrowPoints(),
impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'print', 'print', true)]),
throwPoints: $throwPoints,
impurePoints: array_merge($impurePoints, [new ImpurePoint($scope, $expr, 'print', 'print', true)]),
);
}

Expand Down
16 changes: 16 additions & 0 deletions src/Analyser/ExprHandler/PropertyFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
final class PropertyFetchHandler implements ExprHandler
{

/**
* Representative property name used when resolving dynamic property access ($obj->{$expr}).
* The actual name doesn't matter — it just needs to be non-empty so that
* PropertiesClassReflectionExtensions (e.g. SimpleXMLElement) that accept
* any property name can return the correct type.
*/
private const DYNAMIC_PROPERTY_NAME = '__phpstan_dynamic_property';

public function __construct(
private PhpVersion $phpVersion,
private PropertyReflectionFinder $propertyReflectionFinder,
Expand Down Expand Up @@ -130,6 +138,14 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
);
}

if ($nameType->isString()->yes()) {
$fetchedOnType = $scope->getType($expr->var);
$returnType = $this->propertyFetchType($scope, $fetchedOnType, self::DYNAMIC_PROPERTY_NAME, $expr);
if ($returnType !== null) {
return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType);
}
}

return new MixedType();
}

Expand Down
Loading
Loading