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
48 changes: 23 additions & 25 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2023,18 +2023,7 @@ public function enterAnonymousFunctionWithoutReflection(
$isNullable = $this->isParameterValueNullable($parameter);
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
if ($callableParameters !== null) {
if (isset($callableParameters[$i])) {
$parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
} elseif (count($callableParameters) > 0) {
$lastParameter = array_last($callableParameters);
if ($lastParameter->isVariadic()) {
$parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
} else {
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
}
} else {
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
}
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
}
$holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
$expressionTypes[$paramExprString] = $holder;
Expand Down Expand Up @@ -2233,20 +2222,8 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun
foreach ($arrowFunction->params as $i => $parameter) {
$isNullable = $this->isParameterValueNullable($parameter);
$parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);

if ($callableParameters !== null) {
if (isset($callableParameters[$i])) {
$parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
} elseif (count($callableParameters) > 0) {
$lastParameter = array_last($callableParameters);
if ($lastParameter->isVariadic()) {
$parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
} else {
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
}
} else {
$parameterType = self::intersectButNotNever($parameterType, new MixedType());
}
$parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i));
}

if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
Expand Down Expand Up @@ -2312,6 +2289,27 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this));
}

/**
* @param ParameterReflection[] $callableParameters
*/
private function getCallableParameterType(array $callableParameters, int $index): Type
{
if (isset($callableParameters[$index])) {
return $callableParameters[$index]->getType();
}

if (count($callableParameters) === 0) {
return new MixedType();
}

$lastParameter = array_last($callableParameters);
if ($lastParameter->isVariadic()) {
return $lastParameter->getType();
}

return new MixedType();
}

public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
{
if ($nativeType->isSuperTypeOf($inferredType)->no()) {
Expand Down
8 changes: 7 additions & 1 deletion src/Type/ConditionalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,13 @@

public function isResolvable(): bool
{
return !TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target);
if (!TypeUtils::containsTemplateType($this->subject) && !TypeUtils::containsTemplateType($this->target)) {
return true;
}

$isSuperType = $this->target->isSuperTypeOf($this->subject);

Check warning on line 120 in src/Type/ConditionalType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ return true; } - $isSuperType = $this->target->isSuperTypeOf($this->subject); + $isSuperType = $this->subject->isSuperTypeOf($this->target); return $isSuperType->yes() || $isSuperType->no(); }

Check warning on line 120 in src/Type/ConditionalType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ return true; } - $isSuperType = $this->target->isSuperTypeOf($this->subject); + $isSuperType = $this->subject->isSuperTypeOf($this->target); return $isSuperType->yes() || $isSuperType->no(); }

return $isSuperType->yes() || $isSuperType->no();

Check warning on line 122 in src/Type/ConditionalType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $isSuperType = $this->target->isSuperTypeOf($this->subject); - return $isSuperType->yes() || $isSuperType->no(); + return !$isSuperType->no() || $isSuperType->no(); } protected function getResult(): Type

Check warning on line 122 in src/Type/ConditionalType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $isSuperType = $this->target->isSuperTypeOf($this->subject); - return $isSuperType->yes() || $isSuperType->no(); + return !$isSuperType->no() || $isSuperType->no(); } protected function getResult(): Type
}

protected function getResult(): Type
Expand Down
70 changes: 70 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11894.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug11894Nsrt;

use function PHPStan\Testing\assertType;

/**
* @template T
* @param T $a
* @return (T is string ? string : T)
*/
function conditionalReturn(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}
return trim($a);
}

/**
* @template T of string|null
* @param T $a
*/
function testNarrowedToString(mixed $a): void
{
if (!is_string($a)) {
return;
}
assertType('string', conditionalReturn($a));
}

/**
* @template T of int|null
* @param T $a
*/
function testNarrowedToNonMatchingType(mixed $a): void
{
if (!is_int($a)) {
return;
}
assertType('T of int (function Bug11894Nsrt\testNarrowedToNonMatchingType(), argument)', conditionalReturn($a));
}

/**
* @template T of string|int
* @param T $a
*/
function testNotFullyNarrowable(mixed $a): void
{
assertType('string|T of int (function Bug11894Nsrt\testNotFullyNarrowable(), argument)', conditionalReturn($a));
}

abstract class ConditionalArrayKeys
{
/**
* @template TKey of array-key
* @template TArray of array<TKey, mixed>
* @param TArray $array
* @return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)
*/
abstract public function arrayKeys(array $array): array;

/** @param non-empty-array<int, int> $nonEmpty */
public function testMaybeStaysUnresolved(array $nonEmpty): void
{
assertType('non-empty-list<int>', $this->arrayKeys($nonEmpty));
}
}
35 changes: 35 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8048.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types=1);

namespace Bug8048Nsrt;

use function PHPStan\Testing\assertType;

interface CustomResponseInterface {}

class CustomResponse implements CustomResponseInterface {}

class ApiService
{
/**
* @template T of CustomResponseInterface
*
* @param class-string<T>|null $responseType
*
* @return ($responseType is class-string<T> ? T : null)
*/
public function request(?string $responseType = null): ?CustomResponseInterface
{
if ($responseType === null) {
return null;
}

return new CustomResponse();
}
}

function (): void {
assertType('null', (new ApiService())->request(null));
assertType('Bug8048Nsrt\CustomResponse', (new ApiService())->request(CustomResponse::class));
$x = rand(0, 1) ? CustomResponse::class : null;
assertType('Bug8048Nsrt\CustomResponse|null', (new ApiService())->request($x));
};
Original file line number Diff line number Diff line change
Expand Up @@ -2956,4 +2956,9 @@ public function testBug3842(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []);
}

public function testBug11894(): void
{
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
}

}
79 changes: 79 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-11894.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace Bug11894;

/**
* @template T of string|null
* @param T $a
*/
function test(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

return conditionalReturn($a);
}

/**
* @template T
* @param T $a
* @return (T is string ? string : T)
*/
function conditionalReturn(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

return trim($a);
}

/**
* @template T of string|null
* @param T $a
*/
function testNegated(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

return conditionalReturnNegated($a);
}

/**
* @template T
* @param T $a
* @return (T is not string ? T : string)
*/
function conditionalReturnNegated(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

return trim($a);
}

/**
* @template T of int|null
* @param T $a
*/
function testNoRelation(mixed $a): mixed
{
if (!is_int($a)) {
return $a;
}

return conditionalReturn($a);
}

/**
* @template T of string|int
* @param T $a
*/
function testMaybeRelation(mixed $a): mixed
{
return conditionalReturn($a);
}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4086,4 +4086,20 @@ public function testBug14549(): void
]);
}

public function testBug11894(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
}

public function testBug8048(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-8048.php'], []);
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1031,4 +1031,10 @@ public function testConstantParameterCheckStatic(): void
]);
}

public function testBug11894(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-11894.php'], []);
}

}
81 changes: 81 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-11894.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types = 1);

namespace Bug11894Methods;

class Converter
{
/**
* @template T
* @param T $a
* @return (T is string ? string : T)
*/
public function conditionalReturn(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}
return trim($a);
}

/**
* @template T
* @param T $a
* @return (T is string ? string : T)
*/
public static function conditionalReturnStatic(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}
return trim($a);
}
}

class Consumer
{
/**
* @template T of string|null
* @param T $a
*/
public function testMethod(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

$c = new Converter();
return $c->conditionalReturn($a);
}

/**
* @template T of string|null
* @param T $a
*/
public function testStaticMethod(mixed $a): mixed
{
if (!is_string($a)) {
return $a;
}

return Converter::conditionalReturnStatic($a);
}

/**
* @template T of string|int
* @param T $a
*/
public function testMaybeMethod(mixed $a): mixed
{
$c = new Converter();
return $c->conditionalReturn($a);
}

/**
* @template T of string|int
* @param T $a
*/
public function testMaybeStaticMethod(mixed $a): mixed
{
return Converter::conditionalReturnStatic($a);
}
}
Loading
Loading