Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\Analyser\ExpressionResultFactory;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
use PHPStan\Analyser\ExprHandler\Helper\OutputBufferHelper;
use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer;
use PHPStan\Analyser\ImpurePoint;
Expand Down Expand Up @@ -95,6 +96,7 @@ final class FuncCallHandler implements ExprHandler
* @param ExtensionsCollection<DynamicFunctionThrowTypeExtension> $dynamicFunctionThrowTypeExtensions
*/
public function __construct(
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
private ReflectionProvider $reflectionProvider,
#[AutowiredExtensions(of: DynamicFunctionThrowTypeExtension::class)]
private ExtensionsCollection $dynamicFunctionThrowTypeExtensions,
Expand Down Expand Up @@ -811,6 +813,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra

public function resolveType(MutatingScope $scope, Expr $expr): Type
{
if (
$expr->name instanceof Name
&& $this->earlyTerminatingCallHelper->isEarlyTerminatingFunctionCall($expr->name->toString())
) {
return new NeverType(true);
}

if ($expr->name instanceof Expr) {
$calledOnType = $scope->getType($expr->name);
if ($calledOnType->isCallable()->no()) {
Expand Down
80 changes: 80 additions & 0 deletions src/Analyser/ExprHandler/Helper/EarlyTerminatingCallHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Type;
use function array_key_exists;
use function array_merge;
use function in_array;
use function strtolower;

/**
* Decides whether a method/function call is configured as early-terminating
* (`parameters.earlyTerminatingMethodCalls` / `earlyTerminatingFunctionCalls`).
* The call handlers use this to give such a call an explicit NeverType, from
* which NodeScopeResolver derives the statement's exit point - so the engine no
* longer reaches Scope::getType() to find early-terminating expressions.
*/
#[AutowiredService]
final class EarlyTerminatingCallHelper
{

/** @var array<string, true> */
private array $earlyTerminatingMethodNames;

/**
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
* @param array<int, string> $earlyTerminatingFunctionCalls
*/
public function __construct(
private ReflectionProvider $reflectionProvider,
#[AutowiredParameter]
private array $earlyTerminatingMethodCalls,
#[AutowiredParameter]
private array $earlyTerminatingFunctionCalls,
)
{
$earlyTerminatingMethodNames = [];
foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
foreach ($methodNames as $methodName) {
$earlyTerminatingMethodNames[strtolower($methodName)] = true;
}
}
$this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
}

public function isEarlyTerminatingMethodCall(string $methodName, Type $calledOnType): bool
{
if (!array_key_exists(strtolower($methodName), $this->earlyTerminatingMethodNames)) {
return false;
}

foreach ($calledOnType->getObjectClassNames() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
continue;
}

$classReflection = $this->reflectionProvider->getClass($referencedClass);
foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
continue;
}

if (in_array($methodName, $this->earlyTerminatingMethodCalls[$className], true)) {
return true;
}
}
}

return false;
}

public function isEarlyTerminatingFunctionCall(string $functionName): bool
{
return in_array($functionName, $this->earlyTerminatingFunctionCalls, true);
}

}
9 changes: 9 additions & 0 deletions src/Analyser/ExprHandler/MethodCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Analyser\ExpressionResultFactory;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
Expand Down Expand Up @@ -56,6 +57,7 @@ final class MethodCallHandler implements ExprHandler
{

public function __construct(
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
private MethodThrowPointHelper $methodThrowPointHelper,
private ReflectionProvider $reflectionProvider,
Expand Down Expand Up @@ -246,6 +248,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex

public function resolveType(MutatingScope $scope, Expr $expr): Type
{
if (
$expr->name instanceof Identifier
&& $this->earlyTerminatingCallHelper->isEarlyTerminatingMethodCall($expr->name->name, $scope->getType($expr->var))
) {
return new NeverType(true);
}

if ($expr->name instanceof Identifier) {
if ($scope->nativeTypesPromoted) {
$methodReflection = $scope->getMethodReflection(
Expand Down
11 changes: 11 additions & 0 deletions src/Analyser/ExprHandler/StaticCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Analyser\ExpressionResultFactory;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
Expand Down Expand Up @@ -64,6 +65,7 @@ final class StaticCallHandler implements ExprHandler
{

public function __construct(
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
private MethodThrowPointHelper $methodThrowPointHelper,
private ReflectionProvider $reflectionProvider,
Expand Down Expand Up @@ -302,6 +304,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex

public function resolveType(MutatingScope $scope, Expr $expr): Type
{
if ($expr->name instanceof Identifier) {
$earlyTerminatingClassType = $expr->class instanceof Name
? $scope->resolveTypeByName($expr->class)
: $scope->getType($expr->class);
if ($this->earlyTerminatingCallHelper->isEarlyTerminatingMethodCall($expr->name->name, $earlyTerminatingClassType)) {
return new NeverType(true);
}
}

if ($expr->name instanceof Identifier) {
if ($scope->nativeTypesPromoted) {
if ($expr->class instanceof Name) {
Expand Down
53 changes: 0 additions & 53 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,13 @@ class NodeScopeResolver
/** @var array<string, true> filePath(string) => bool(true) */
private array $analysedFiles = [];

/** @var array<string, true> */
private array $earlyTerminatingMethodNames;

/** @var array<string, true> */
private array $calledMethodStack = [];

/** @var array<string, MutatingScope|null> */
private array $calledMethodResults = [];

/**
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
* @param array<int, string> $earlyTerminatingFunctionCalls
* @param ExtensionsCollection<FunctionParameterOutTypeExtension> $functionParameterOutTypeExtensions
* @param ExtensionsCollection<MethodParameterOutTypeExtension> $methodParameterOutTypeExtensions
* @param ExtensionsCollection<StaticMethodParameterOutTypeExtension> $staticMethodParameterOutTypeExtensions
Expand Down Expand Up @@ -268,10 +263,6 @@ public function __construct(
private readonly bool $polluteScopeWithAlwaysIterableForeach,
#[AutowiredParameter]
private readonly bool $polluteScopeWithBlock,
#[AutowiredParameter]
private readonly array $earlyTerminatingMethodCalls,
#[AutowiredParameter]
private readonly array $earlyTerminatingFunctionCalls,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private readonly bool $implicitThrows,
#[AutowiredParameter]
Expand All @@ -280,13 +271,6 @@ public function __construct(
protected readonly ExpressionResultFactory $expressionResultFactory,
)
{
$earlyTerminatingMethodNames = [];
foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
foreach ($methodNames as $methodName) {
$earlyTerminatingMethodNames[strtolower($methodName)] = true;
}
}
$this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
}

/**
Expand Down Expand Up @@ -2758,43 +2742,6 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo

private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
{
if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
if ($expr instanceof MethodCall) {
$methodCalledOnType = $scope->getType($expr->var);
} else {
if ($expr->class instanceof Name) {
$methodCalledOnType = $scope->resolveTypeByName($expr->class);
} else {
$methodCalledOnType = $scope->getType($expr->class);
}
}

foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
continue;
}

$classReflection = $this->reflectionProvider->getClass($referencedClass);
foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
continue;
}

if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
return $expr;
}
}
}
}
}

if ($expr instanceof FuncCall && $expr->name instanceof Name) {
if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
return $expr;
}
}

if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
return $expr;
}
Expand Down
2 changes: 0 additions & 2 deletions src/Testing/RuleTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver
$this->shouldPolluteScopeWithLoopInitialAssignments(),
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
self::getContainer()->getParameter('polluteScopeWithBlock'),
[],
[],
self::getContainer()->getParameter('exceptions')['implicitThrows'],
$this->shouldTreatPhpDocTypesAsCertain(),
self::getContainer()->getByType(ImplicitToStringCallHelper::class),
Expand Down
14 changes: 0 additions & 14 deletions src/Testing/TypeInferenceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver
$container->getParameter('polluteScopeWithLoopInitialAssignments'),
$container->getParameter('polluteScopeWithAlwaysIterableForeach'),
$container->getParameter('polluteScopeWithBlock'),
static::getEarlyTerminatingMethodCalls(),
static::getEarlyTerminatingFunctionCalls(),
$container->getParameter('exceptions')['implicitThrows'],
$container->getParameter('treatPhpDocTypesAsCertain'),
$container->getByType(ImplicitToStringCallHelper::class),
Expand Down Expand Up @@ -500,16 +498,4 @@ protected static function getAdditionalAnalysedFiles(): array
return [];
}

/** @return string[][] */
protected static function getEarlyTerminatingMethodCalls(): array
{
return [];
}

/** @return string[] */
protected static function getEarlyTerminatingFunctionCalls(): array
{
return [];
}

}
2 changes: 0 additions & 2 deletions tests/PHPStan/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -842,8 +842,6 @@ private function createAnalyser(): Analyser
false,
true,
true,
[],
[],
true,
$this->shouldTreatPhpDocTypesAsCertain(),
$container->getByType(ImplicitToStringCallHelper::class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver
$this->shouldPolluteScopeWithLoopInitialAssignments(),
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
self::getContainer()->getParameter('polluteScopeWithBlock'),
[],
[],
self::getContainer()->getParameter('exceptions')['implicitThrows'],
$this->shouldTreatPhpDocTypesAsCertain(),
self::getContainer()->getByType(ImplicitToStringCallHelper::class),
Expand Down
2 changes: 0 additions & 2 deletions tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver
$container->getParameter('polluteScopeWithLoopInitialAssignments'),
$container->getParameter('polluteScopeWithAlwaysIterableForeach'),
$container->getParameter('polluteScopeWithBlock'),
static::getEarlyTerminatingMethodCalls(),
static::getEarlyTerminatingFunctionCalls(),
$container->getParameter('exceptions')['implicitThrows'],
$container->getParameter('treatPhpDocTypesAsCertain'),
$container->getByType(ImplicitToStringCallHelper::class),
Expand Down
16 changes: 1 addition & 15 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static function getAdditionalConfigFiles(): array
return [
__DIR__ . '/../../../conf/bleedingEdge.neon',
__DIR__ . '/typeAliases.neon',
__DIR__ . '/nodeScopeResolverEarlyTerminating.neon',
];
}

Expand Down Expand Up @@ -92,19 +93,4 @@ public function testEarlyTermination(): void
});
}

protected static function getEarlyTerminatingMethodCalls(): array
{
return [
\EarlyTermination\Foo::class => [
'doFoo',
'doBar',
],
];
}

protected static function getEarlyTerminatingFunctionCalls(): array
{
return ['baz'];
}

}
16 changes: 1 addition & 15 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ public static function getAdditionalConfigFiles(): array
[
__DIR__ . '/../../../conf/bleedingEdge.neon',
__DIR__ . '/typeAliases.neon',
__DIR__ . '/nodeScopeResolverEarlyTerminating.neon',
],
);
}
Expand All @@ -369,19 +370,4 @@ protected static function getAdditionalAnalysedFiles(): array
];
}

protected static function getEarlyTerminatingMethodCalls(): array
{
return [
\EarlyTermination\Foo::class => [
'doFoo',
'doBar',
],
];
}

protected static function getEarlyTerminatingFunctionCalls(): array
{
return ['baz'];
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Analyser/nodeScopeResolverEarlyTerminating.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
earlyTerminatingMethodCalls:
EarlyTermination\Foo:
- doFoo
- doBar
earlyTerminatingFunctionCalls:
- baz
Loading