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
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,12 @@ parameters:
count: 2
path: src/Rules/RuleErrorBuilder.php

-
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 1
path: src/Rules/RuleLevelHelper.php

-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
Expand Down
35 changes: 33 additions & 2 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\CallableType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateMixedType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -49,6 +51,29 @@ public function __construct(
{
}

/**
* @param callable(Type): Type $traverse
*/
private function traverseWithoutMapping(Type $type, ?Type $acceptingType, callable $traverse): Type
{
if ($type instanceof GenericObjectType && $acceptingType !== null) {
return $type->traverseSimultaneously($acceptingType, function (Type $type, Type $acceptingType) use ($traverse): Type {
if (
!$this->checkNullables
&& !$type instanceof BenevolentUnionType
&& TypeCombinator::containsNull($type)
&& !TypeCombinator::containsNull($acceptingType)
) {
return $traverse(TypeCombinator::removeNull($type));
}

return $type;
});
}

return $traverse($type);
}

/** @api */
public function isThis(Expr $expression): bool
{
Expand Down Expand Up @@ -93,9 +118,10 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
return $acceptedType;
}

$acceptingReturnType = $acceptingType instanceof ParametersAcceptor ? $acceptingType->getReturnType() : null;
return new CallableType(
$acceptedType->getParameters(),
$traverse($acceptedType->getReturnType()),
$this->traverseWithoutMapping($acceptedType->getReturnType(), $acceptingReturnType, $traverse),
$acceptedType->isVariadic(),
$acceptedType->getTemplateTypeMap(),
$acceptedType->getResolvedTemplateTypeMap(),
Expand All @@ -109,9 +135,10 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
return $acceptedType;
}

$acceptingReturnType = $acceptingType instanceof ParametersAcceptor ? $acceptingType->getReturnType() : null;
return new ClosureType(
$acceptedType->getParameters(),
$traverse($acceptedType->getReturnType()),
$this->traverseWithoutMapping($acceptedType->getReturnType(), $acceptingReturnType, $traverse),
$acceptedType->isVariadic(),
$acceptedType->getTemplateTypeMap(),
$acceptedType->getResolvedTemplateTypeMap(),
Expand All @@ -127,6 +154,10 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
);
}

if ($acceptedType instanceof GenericObjectType) {
return $this->traverseWithoutMapping($acceptedType, $acceptingType, $traverse);
}

if (
!$this->checkNullables
&& !$acceptingType instanceof NullType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
class CallToFunctionParametersRuleTest extends RuleTestCase
{

private bool $checkNullables = true;

private bool $checkExplicitMixed = false;

private bool $checkImplicitMixed = false;
Expand All @@ -32,7 +34,7 @@ protected function getRule(): Rule
new FunctionCallParametersCheck(
new RuleLevelHelper(
$broker,
checkNullables: true,
checkNullables: $this->checkNullables,
checkThisOnly: false,
checkUnionTypes: true,
checkExplicitMixed: $this->checkExplicitMixed,
Expand Down Expand Up @@ -3109,4 +3111,16 @@ public function testBug15168(): void
$this->analyse([__DIR__ . '/data/bug-15168.php'], []);
}

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

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

}
10 changes: 9 additions & 1 deletion tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
class ClosureReturnTypeRuleTest extends RuleTestCase
{

private bool $checkNullables = true;

protected function getRule(): Rule
{
return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(
new RuleLevelHelper(
self::createReflectionProvider(),
checkNullables: true,
checkNullables: $this->checkNullables,
checkThisOnly: false,
checkUnionTypes: true,
checkExplicitMixed: false,
Expand Down Expand Up @@ -154,4 +156,10 @@ public function testBug14914(): void
$this->analyse([__DIR__ . '/data/bug-14914.php'], []);
}

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

}
41 changes: 41 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-11041.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php // lint >= 7.4

declare(strict_types = 1);

namespace Bug11041;

/**
* @template TKey
* @template TValue
*/
class Collection
{

/** @var array<TKey, TValue> */
private array $items;

/** @param array<TKey, TValue> $items */
public function __construct(array $items)
{
$this->items = $items;
}

/**
* @param TKey $key
* @return TValue
*/
public function get($key)
{
return $this->items[$key];
}

}

/** @param Collection<int, string|null> $collection */
function testFunc(Collection $collection): void
{
}

$collection = new Collection([0 => 'foo', 1 => 'bar', 2 => null, 3 => 'baz']);

testFunc($collection);
54 changes: 54 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-12008.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug12008;

use Closure;

interface ProductOverview
{

public function getId(): ?int;

}

/**
* @template T
*/
readonly class Pagination
{

/**
* @param iterable<T> $records
*/
public function __construct(
public iterable $records,
)
{
}

}

class HelloWorld
{

private function respondToApiRequest(Closure|null $data): never
{
exit;
}

/** @param list<ProductOverview> $products */
public function run(array $products): never
{
$this->respondToApiRequest(function () use ($products) {
return new Pagination(array_map(
fn (ProductOverview $product) => [
'id' => $product->getId(),
],
$products,
));
});
}

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

namespace Bug9377;

/**
* @template T
*/
class HelloWorld
{

}

/** @param HelloWorld<array{id: int|null}> $foo */
function foo($foo): void
{
}

/** @return HelloWorld<array{id: int|null}> */
function bar(): HelloWorld
{
return new HelloWorld();
}

$a = bar();

foo($a);
36 changes: 36 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3023,6 +3023,42 @@ public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $c
$this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors);
}

public static function dataGenericArgumentNullability(): iterable
{
yield [false, [
[
'Parameter #1 $collection of method GenericArgumentNullability\\Foo::acceptNullable() expects GenericArgumentNullability\\Collection<string, int|null>, GenericArgumentNullability\\Collection<string, int> given.',
75,
'Template type TValue on class GenericArgumentNullability\\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
]];
yield [true, [
[
'Parameter #1 $collection of method GenericArgumentNullability\\Foo::acceptPlain() expects GenericArgumentNullability\\Collection<string, int>, GenericArgumentNullability\\Collection<string, int|null> given.',
74,
],
[
'Parameter #1 $collection of method GenericArgumentNullability\\Foo::acceptNullable() expects GenericArgumentNullability\\Collection<string, int|null>, GenericArgumentNullability\\Collection<string, int> given.',
75,
'Template type TValue on class GenericArgumentNullability\\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $collection of method GenericArgumentNullability\\Foo::acceptCovariantPlain() expects GenericArgumentNullability\\CovariantCollection<int>, GenericArgumentNullability\\CovariantCollection<int|null> given.',
76,
],
]];
}

/** @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrors */
#[DataProvider('dataGenericArgumentNullability')]
public function testGenericArgumentNullability(bool $checkNullables, array $expectedErrors): void
{
$this->checkThisOnly = false;
$this->checkNullables = $checkNullables;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/generic-argument-nullability.php'], $expectedErrors);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug8713(): void
{
Expand Down
11 changes: 10 additions & 1 deletion tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
class CallStaticMethodsRuleTest extends RuleTestCase
{

private bool $checkNullables = true;

private bool $checkThisOnly;

private bool $checkExplicitMixed = false;
Expand All @@ -36,7 +38,7 @@ protected function getRule(): Rule
$reflectionProvider = self::createReflectionProvider();
$ruleLevelHelper = new RuleLevelHelper(
$reflectionProvider,
checkNullables: true,
checkNullables: $this->checkNullables,
checkThisOnly: $this->checkThisOnly,
checkUnionTypes: true,
checkExplicitMixed: $this->checkExplicitMixed,
Expand Down Expand Up @@ -1084,4 +1086,11 @@ public function testBug12827(): void
]);
}

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

}
10 changes: 9 additions & 1 deletion tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
class ReturnTypeRuleTest extends RuleTestCase
{

private bool $checkNullables = true;

private bool $checkExplicitMixed = false;

private bool $checkUnionTypes = true;
Expand All @@ -27,7 +29,7 @@ protected function getRule(): Rule
return new ReturnTypeRule(new FunctionReturnTypeCheck(
new RuleLevelHelper(
self::createReflectionProvider(),
checkNullables: true,
checkNullables: $this->checkNullables,
checkThisOnly: false,
checkUnionTypes: $this->checkUnionTypes,
checkExplicitMixed: $this->checkExplicitMixed,
Expand Down Expand Up @@ -1364,4 +1366,10 @@ public function testBug14893(): void
$this->analyse([__DIR__ . '/data/bug-14893.php'], []);
}

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

}
Loading
Loading