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
32 changes: 30 additions & 2 deletions src/Analyser/ClosureProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ private function getParametersProcessor(): ParametersProcessor
return $this->container->getByType(ParametersProcessor::class);
}

/**
* Looked up instead of injected for the same reason as getParametersProcessor().
*/
private function getAttributesHandler(): AttributesHandler
{
return $this->container->getByType(AttributesHandler::class);
}

/**
* Returns what the attributes and the parameters of the closure or arrow function depend on.
*
* @param callable(Node $node, Scope $scope): void $nodeCallback
*/
private function processAttributeGroupsAndParams(
NodeScopeResolver $nodeScopeResolver,
Node\Stmt $stmt,
Expr\Closure|Expr\ArrowFunction $expr,
MutatingScope $scope,
ExpressionResultStorage $storage,
callable $nodeCallback,
): ?Dependencies
{
return Dependencies::merge(
$this->getAttributesHandler()->processAttributeGroups($nodeScopeResolver, $stmt, $expr->attrGroups, $scope, $storage, $nodeCallback),
$this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback),
);
}

/**
* @param callable(Node $node, Scope $scope): void $nodeCallback
*/
Expand Down Expand Up @@ -107,7 +135,7 @@ private function processClosureNodeInternal(
?Type $nativePassedToType = null,
): ProcessClosureResult
{
$parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
$parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback);

$byRefUses = [];

Expand Down Expand Up @@ -756,7 +784,7 @@ public function processArrowFunctionNode(
): ProcessArrowFunctionResult
{
$context ??= ExpressionContext::createTopLevel();
$parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
$parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback);
if ($this->closureSignatureInference->isObserving($scope)) {
foreach (ClosureSignatureInference::getArrowFunctionOuterVariables($expr) as $name) {
if (!$scope->hasVariableType($name)->yes()) {
Expand Down
1 change: 1 addition & 0 deletions src/Analyser/ExprHandler/ClassConstFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ static function (Expr $e) use ($classResult, $nativeTypesPromoted): Type {

return $nativeTypesPromoted ? $classResult->getNativeType() : $classResult->getType();
},
$nativeTypesPromoted,
);
},
specifyTypesCallback: fn (TypeSpecifierContext $context, bool $nativeTypesPromoted) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context),
Expand Down
11 changes: 11 additions & 0 deletions src/Analyser/StmtHandler/TryCatchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ public function processStmt(

if (count($matchingThrowPoints) === 0) {
$catchFlows[] = [$originalCatchType, $this->statementsHandler->getVariableMentionFlow($catchNode)];

// the catch block is not reachable, but rules still need to see its statements;
// it is entered from where the try block can throw anything, and the result
// of the walk does not flow anywhere
$deadCatchScope = null;
foreach ($branchScopeResult->getThrowPoints() as $throwPoint) {
$deadCatchScope = $deadCatchScope === null ? $throwPoint->getScope() : $deadCatchScope->mergeWith($throwPoint->getScope());
}
$deadCatchScope = ($deadCatchScope ?? $scope->mergeWith($branchScope))->enterCatchType($catchType instanceof NeverType ? $originalCatchType : $catchType, $catchNode->var !== null && is_string($catchNode->var->name) ? $catchNode->var->name : null);
$deadCatchResult = $nodeScopeResolver->processStmtNodesInternal($catchNode, $catchNode->stmts, $deadCatchScope, $storage, $nodeCallback, $context);
$dependencies[] = $deadCatchResult->getDependencies();
continue;
}

Expand Down
28 changes: 17 additions & 11 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2682,9 +2682,12 @@ private static function shiftLeftOverflows(int $value, int $shift): bool
}

/**
* With $nativeTypesPromoted, PHPDoc types of the constants are ignored:
* a constant that can be overridden in a child class resolves only to its native type.
*
* @param callable(Expr): Type $getTypeCallback
*/
public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type
public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback, bool $nativeTypesPromoted = false): Type
{
$isObject = false;
if ($class instanceof Name) {
Expand Down Expand Up @@ -2762,8 +2765,9 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
continue;
}

if (!$isObject && array_key_exists($resolvingName, $this->classConstantValueTypeCache)) {
$types[] = $this->classConstantValueTypeCache[$resolvingName];
$cacheKey = $nativeTypesPromoted ? $resolvingName . '#native' : $resolvingName;
if (!$isObject && array_key_exists($cacheKey, $this->classConstantValueTypeCache)) {
$types[] = $this->classConstantValueTypeCache[$cacheKey];
continue;
}

Expand All @@ -2786,9 +2790,9 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
$constantName,
$constantType,
$nativeType,
$constantClassReflection->getConstantPhpDocType($constantName),
$nativeTypesPromoted ? null : $constantClassReflection->getConstantPhpDocType($constantName),
), $fetchedOnType);
$this->classConstantValueTypeCache[$resolvingName] = $resolvedType;
$this->classConstantValueTypeCache[$cacheKey] = $resolvedType;
$types[] = $resolvedType;
unset($this->currentlyResolvingClassConstant[$resolvingName]);
continue;
Expand All @@ -2798,26 +2802,28 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
if (
!$constantClassReflection->isFinal()
&& !$constantReflection->isFinal()
&& !$constantReflection->hasPhpDocType()
&& ($nativeTypesPromoted || !$constantReflection->hasPhpDocType())
&& !$constantReflection->hasNativeType()
) {
unset($this->currentlyResolvingClassConstant[$resolvingName]);
return new MixedType();
}

if (!$constantClassReflection->isFinal()) {
$constantType = $constantReflection->getValueType();
} else {
$nativeType = $constantReflection->getNativeType();
if ($constantClassReflection->isFinal() || ($nativeTypesPromoted && $constantReflection->isFinal())) {
$constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass()));
} elseif ($nativeTypesPromoted && $nativeType !== null) {
$constantType = $nativeType;
} else {
$constantType = $constantReflection->getValueType();
}

$nativeType = $constantReflection->getNativeType();
$constantType = $this->transformStaticType($this->constantResolver->resolveClassConstantType(
$constantClassReflection->getName(),
$constantName,
$constantType,
$nativeType,
$constantClassReflection->getConstantPhpDocType($constantName),
$nativeTypesPromoted ? null : $constantClassReflection->getConstantPhpDocType($constantName),
), $fetchedOnType);
unset($this->currentlyResolvingClassConstant[$resolvingName]);
$types[] = $constantType;
Expand Down
58 changes: 58 additions & 0 deletions src/Reflection/SignatureMap/Php8SignatureMapProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,64 @@ public function hasFunctionSignature(string $name): bool
}

public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array
{
return $this->applyTentativeReturnType(
$className,
$reflectionMethod,
$this->getMethodSignaturesWithoutTentativeReturnType($className, $methodName, $reflectionMethod),
);
}

/**
* PHP checks a tentative return type of a built-in method like a real one,
* and overriding it with an incompatible type is deprecated,
* so it is used as the native return type when the signature does not have one.
*
* @param array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>} $signatures
* @return array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>}
*/
private function applyTentativeReturnType(string $className, ?ReflectionMethod $reflectionMethod, array $signatures): array
{
if ($reflectionMethod === null || !$this->phpVersion->hasTentativeReturnTypes()) {
return $signatures;
}

$tentativeReturnType = $reflectionMethod->getTentativeReturnType();
if ($tentativeReturnType === null) {
return $signatures;
}

$nativeReturnType = TypehintHelper::decideTypeFromReflection(
$tentativeReturnType,
selfClass: $this->reflectionProviderProvider->getReflectionProvider()->getClass($className),
);
foreach ($signatures as $signatureType => $typeSignatures) {
if ($typeSignatures === null) {
continue;
}

foreach ($typeSignatures as $i => $signature) {
$signatureNativeReturnType = $signature->getNativeReturnType();
if (!$signatureNativeReturnType instanceof MixedType || $signatureNativeReturnType->isExplicitMixed()) {
continue;
}

$signatures[$signatureType][$i] = new FunctionSignature(
$signature->getParameters(),
$signature->getReturnType(),
$nativeReturnType,
$signature->isVariadic(),
);
}
}

return $signatures;
}

/**
* @return array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>}
*/
private function getMethodSignaturesWithoutTentativeReturnType(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array
{
$lowerClassName = strtolower($className);
if (!array_key_exists($lowerClassName, $this->map->classes)) {
Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Methods/OverridingMethodRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
$reportReturnType = !$realPrototype instanceof MethodPrototypeReflection
|| $realPrototype->getTentativeReturnType() === null
|| (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no());
} elseif ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->getTentativeReturnType() !== null) {
// return types that are only tentative are not checked before PHP 8.1
$reportReturnType = false;
} else {
if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) {
if (
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-14397.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function test2(mixed $mixed): void
filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE);
$foo = 1;
} catch (\Filter\FilterFailedException $e) {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
assertVariableCertainty(TrinaryLogic::createMaybe(), $foo);
}

assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE));
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/class-constant-native-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ClassConstantNativeType;

use function PHPStan\Testing\assertNativeType;
use function PHPStan\Testing\assertType;

class Foo
Expand All @@ -14,6 +15,9 @@ public function doFoo(): void
assertType('1', self::FOO);
assertType('int', static::FOO);
assertType('int', $this::FOO);
assertNativeType('1', self::FOO);
assertNativeType('int', static::FOO);
assertNativeType('int', $this::FOO);
}

}
Expand Down Expand Up @@ -43,6 +47,9 @@ public function doFoo(): void
assertType('1', self::FOO);
assertType('int<1, max>', static::FOO);
assertType('int<1, max>', $this::FOO);
assertNativeType('1', self::FOO);
assertNativeType('int', static::FOO);
assertNativeType('int', $this::FOO);
}

}
Expand All @@ -61,3 +68,41 @@ public function doFoo(): void
}

}

class FooWithOnlyPhpDoc
{

/** @var non-empty-string */
public const FOO = 'foo';

/** @var non-empty-string */
final public const FINAL_FOO = 'foo';

public function doFoo(self $foo): void
{
assertType('\'foo\'', self::FOO);
assertType('non-empty-string', static::FOO);
assertType('non-empty-string', $foo::FOO);
assertNativeType('\'foo\'', self::FOO);
assertNativeType('mixed', static::FOO);
assertNativeType('mixed', $foo::FOO);

assertType('non-empty-string', static::FINAL_FOO);
assertNativeType('\'foo\'', static::FINAL_FOO);
}

}

final class FinalFooWithOnlyPhpDoc
{

/** @var non-empty-string */
public const FOO = 'foo';

public function doFoo(): void
{
assertType('\'foo\'', static::FOO);
assertNativeType('\'foo\'', static::FOO);
}

}
Loading
Loading