Skip to content

Commit

Permalink
Dynamic throw type extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 25, 2021
1 parent a00eb3f commit 2bf30bf
Show file tree
Hide file tree
Showing 19 changed files with 596 additions and 54 deletions.
9 changes: 9 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ services:
class: PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider
factory: PHPStan\DependencyInjection\Type\LazyOperatorTypeSpecifyingExtensionRegistryProvider

-
class: PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider
factory: PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider

-
class: PHPStan\File\FileHelper
arguments:
Expand Down Expand Up @@ -1021,6 +1025,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\JsonThrowTypeExtension
tags:
- phpstan.dynamicFunctionThrowTypeExtension

-
class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension
tags:
Expand Down
175 changes: 127 additions & 48 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\File\FileHelper;
use PHPStan\File\FileReader;
use PHPStan\Node\BooleanAndNode;
Expand Down Expand Up @@ -145,6 +146,8 @@ class NodeScopeResolver

private \PHPStan\Analyser\TypeSpecifier $typeSpecifier;

private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider;

private bool $polluteScopeWithLoopInitialAssignments;

private bool $polluteCatchScopeWithTryAssignments;
Expand Down Expand Up @@ -190,6 +193,7 @@ public function __construct(
PhpDocInheritanceResolver $phpDocInheritanceResolver,
FileHelper $fileHelper,
TypeSpecifier $typeSpecifier,
DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
bool $polluteScopeWithLoopInitialAssignments,
bool $polluteCatchScopeWithTryAssignments,
bool $polluteScopeWithAlwaysIterableForeach,
Expand All @@ -208,6 +212,7 @@ public function __construct(
$this->phpDocInheritanceResolver = $phpDocInheritanceResolver;
$this->fileHelper = $fileHelper;
$this->typeSpecifier = $typeSpecifier;
$this->dynamicThrowTypeExtensionProvider = $dynamicThrowTypeExtensionProvider;
$this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments;
$this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments;
$this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach;
Expand Down Expand Up @@ -1780,34 +1785,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());

if (isset($functionReflection)) {
if ($functionReflection->getThrowType() !== null) {
$throwType = $functionReflection->getThrowType();
if (!$throwType instanceof VoidType) {
$throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true);
}
} elseif ($this->implicitThrows) {
$requiredParameters = null;
if ($parametersAcceptor !== null) {
$requiredParameters = 0;
foreach ($parametersAcceptor->getParameters() as $parameter) {
if ($parameter->isOptional()) {
continue;
}

$requiredParameters++;
}
}
if (
!$functionReflection->isBuiltin()
|| $requiredParameters === null
|| $requiredParameters > 0
|| count($expr->args) > 0
) {
$functionReturnedType = $scope->getType($expr);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
$throwPoints[] = ThrowPoint::createImplicit($scope);
}
}
$functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope);
if ($functionThrowPoint !== null) {
$throwPoints[] = $functionThrowPoint;
}
} else {
$throwPoints[] = ThrowPoint::createImplicit($scope);
Expand Down Expand Up @@ -1975,16 +1955,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
$expr->args,
$methodReflection->getVariants()
);
if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType instanceof VoidType) {
$throwPoints[] = ThrowPoint::createExplicit($scope, $methodReflection->getThrowType(), true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($expr);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
$throwPoints[] = ThrowPoint::createImplicit($scope);
}
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $expr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}
} else {
$throwPoints[] = ThrowPoint::createImplicit($scope);
Expand Down Expand Up @@ -2056,16 +2029,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression
$expr->args,
$methodReflection->getVariants()
);
if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType instanceof VoidType) {
$throwPoints[] = ThrowPoint::createExplicit($scope, $throwType, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($expr);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
$throwPoints[] = ThrowPoint::createImplicit($scope);
}
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope);
if ($methodThrowPoint !== null) {
$throwPoints[] = $methodThrowPoint;
}
if (
$classReflection->getName() === 'Closure'
Expand Down Expand Up @@ -2627,6 +2593,119 @@ static function () use ($scope, $expr): MutatingScope {
);
}

private function getFunctionThrowPoint(
FunctionReflection $functionReflection,
?ParametersAcceptor $parametersAcceptor,
FuncCall $funcCall,
MutatingScope $scope
): ?ThrowPoint
{
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) {
if (!$extension->isFunctionSupported($functionReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $funcCall, $scope);
if ($throwType === null) {
return null;
}

return ThrowPoint::createExplicit($scope, $throwType, false);
}

if ($functionReflection->getThrowType() !== null) {
$throwType = $functionReflection->getThrowType();
if (!$throwType instanceof VoidType) {
return ThrowPoint::createExplicit($scope, $throwType, true);
}
} elseif ($this->implicitThrows) {
$requiredParameters = null;
if ($parametersAcceptor !== null) {
$requiredParameters = 0;
foreach ($parametersAcceptor->getParameters() as $parameter) {
if ($parameter->isOptional()) {
continue;
}

$requiredParameters++;
}
}
if (
!$functionReflection->isBuiltin()
|| $requiredParameters === null
|| $requiredParameters > 0
|| count($funcCall->args) > 0
) {
$functionReturnedType = $scope->getType($funcCall);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
return ThrowPoint::createImplicit($scope);
}
}
}

return null;
}

private function getMethodThrowPoint(MethodReflection $methodReflection, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint
{
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
if (!$extension->isMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $methodCall, $scope);
if ($throwType === null) {
return null;
}

return ThrowPoint::createExplicit($scope, $throwType, false);
}

if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType instanceof VoidType) {
return ThrowPoint::createExplicit($scope, $throwType, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($methodCall);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
return ThrowPoint::createImplicit($scope);
}
}

return null;
}

private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint
{
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
if (!$extension->isStaticMethodSupported($methodReflection)) {
continue;
}

$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $methodCall, $scope);
if ($throwType === null) {
return null;
}

return ThrowPoint::createExplicit($scope, $throwType, false);
}

if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType instanceof VoidType) {
return ThrowPoint::createExplicit($scope, $throwType, true);
}
} elseif ($this->implicitThrows) {
$methodReturnedType = $scope->getType($methodCall);
if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
return ThrowPoint::createImplicit($scope);
}
}

return null;
}

/**
* @param Expr $expr
* @return string[]
Expand Down
21 changes: 21 additions & 0 deletions src/DependencyInjection/Type/DynamicThrowTypeExtensionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\Type\DynamicFunctionThrowTypeExtension;
use PHPStan\Type\DynamicMethodThrowTypeExtension;
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;

interface DynamicThrowTypeExtensionProvider
{

/** @return DynamicFunctionThrowTypeExtension[] */
public function getDynamicFunctionThrowTypeExtensions(): array;

/** @return DynamicMethodThrowTypeExtension[] */
public function getDynamicMethodThrowTypeExtensions(): array;

/** @return DynamicStaticMethodThrowTypeExtension[] */
public function getDynamicStaticMethodThrowTypeExtensions(): array;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection\Type;

use PHPStan\DependencyInjection\Container;

class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtensionProvider
{

public const FUNCTION_TAG = 'phpstan.dynamicFunctionThrowTypeExtension';
public const METHOD_TAG = 'phpstan.dynamicMethodThrowTypeExtension';
public const STATIC_METHOD_TAG = 'phpstan.dynamicStaticMethodThrowTypeExtension';

private Container $container;

public function __construct(Container $container)
{
$this->container = $container;
}

public function getDynamicFunctionThrowTypeExtensions(): array
{
return $this->container->getServicesByTag(self::FUNCTION_TAG);
}

public function getDynamicMethodThrowTypeExtensions(): array
{
return $this->container->getServicesByTag(self::METHOD_TAG);
}

public function getDynamicStaticMethodThrowTypeExtensions(): array
{
return $this->container->getServicesByTag(self::STATIC_METHOD_TAG);
}

}
2 changes: 2 additions & 0 deletions src/Testing/RuleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Cache\Cache;
use PHPStan\Dependency\DependencyResolver;
use PHPStan\Dependency\ExportedNodeResolver;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\File\FileHelper;
use PHPStan\File\SimpleRelativePathHelper;
use PHPStan\Php\PhpVersion;
Expand Down Expand Up @@ -79,6 +80,7 @@ private function getAnalyser(): Analyser
$phpDocInheritanceResolver,
$fileHelper,
$typeSpecifier,
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
$this->shouldPolluteScopeWithLoopInitialAssignments(),
$this->shouldPolluteCatchScopeWithTryAssignments(),
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
Expand Down
2 changes: 2 additions & 0 deletions src/Testing/TypeInferenceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\Broker\AnonymousClassNameHelper;
use PHPStan\Broker\Broker;
use PHPStan\Cache\Cache;
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
use PHPStan\File\FileHelper;
use PHPStan\File\SimpleRelativePathHelper;
use PHPStan\Php\PhpVersion;
Expand Down Expand Up @@ -71,6 +72,7 @@ public function processFile(
$phpDocInheritanceResolver,
$fileHelper,
$typeSpecifier,
self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class),
true,
$this->polluteCatchScopeWithTryAssignments,
true,
Expand Down
16 changes: 16 additions & 0 deletions src/Type/DynamicFunctionThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;

interface DynamicFunctionThrowTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool;

public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type;

}
16 changes: 16 additions & 0 deletions src/Type/DynamicMethodThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;

interface DynamicMethodThrowTypeExtension
{

public function isMethodSupported(MethodReflection $methodReflection): bool;

public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type;

}
16 changes: 16 additions & 0 deletions src/Type/DynamicStaticMethodThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;

interface DynamicStaticMethodThrowTypeExtension
{

public function isStaticMethodSupported(MethodReflection $methodReflection): bool;

public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type;

}
Loading

0 comments on commit 2bf30bf

Please sign in to comment.