Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solve https://github.com/phpstan/phpstan/issues/5348 #586

Merged
merged 4 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ parameters:
excludes_analyse:
- ../src/Reflection/SignatureMap/functionMap.php
- ../src/Reflection/SignatureMap/functionMetadata.php
- ../tests/*/data/*
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For local test

- ../tests/tmp/*
- ../tests/PHPStan/Analyser/traits/*
- ../tests/notAutoloaded/*
Expand Down
9 changes: 9 additions & 0 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;

class RuleLevelHelper
{
Expand Down Expand Up @@ -76,6 +77,14 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp
}

if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {
if (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I don't see why RuleLevelHelper would have to be changed for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know why but there is an early check here:

When I check \DateTimeInterface|int vs \DateTime|\DateTimeImmutable|int it's a compound type, so this is not done here and ends up in the UnionType::accepts methods.
But when I check \DateTimeInterface vs \DateTime|\DateTimeImmutable there is an early check here which return either true or false, so the UnionType::accepts method is never executed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is about DateTime|DateTimeImmutable accepting DateTimeInterface. So only UnionType::accepts() should be modified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acceptingType = DateTime|DateTimeImmutable
acceptedType = DateTimeInterface

if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {

We're in this if.

So we're currently doing

foreach ($acceptingType->getTypes() as $innerType) {
     if (self::accepts($innerType, $acceptedType, $strictTypes)) {
          return true;
     }
 }
return false;

and since neither DateTime doesn't accept DateTimeInterface, neither DateTimeImmutable accept DateTimeInterface, false is returned.

I can change the check

if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {

to something like

if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType && !$acceptedType->equals(new ObjectType(\DateTimeInterface::class))) {

if you prefer, but something has to be done here.

If I remove the code added here, tests are failing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT @ondrejmirtes ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in RuleLevelHelper shouldn't be specific to the DateTime situation, but more general. I propose something like this:

diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php
index 240258921..7fc9724b8 100644
--- a/src/Rules/RuleLevelHelper.php
+++ b/src/Rules/RuleLevelHelper.php
@@ -75,7 +75,8 @@ class RuleLevelHelper
 			$acceptedType = TypeCombinator::removeNull($acceptedType);
 		}
 
-		if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {
+		$accepts = $acceptingType->accepts($acceptedType, $strictTypes);
+		if (!$accepts->yes() && $acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) {
 			foreach ($acceptingType->getTypes() as $innerType) {
 				if (self::accepts($innerType, $acceptedType, $strictTypes)) {
 					return true;
@@ -103,8 +104,6 @@ class RuleLevelHelper
 			);
 		}
 
-		$accepts = $acceptingType->accepts($acceptedType, $strictTypes);
-
 		return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no();
 	}
 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I didn't like to be specific to the DateTime but It wasn't clear to me what to do.

I used your proposal and seems to work perfectly ; thanks :)

$acceptedType->describe(VerbosityLevel::precise()) === 'DateTimeInterface'
&& str_contains($acceptingType->describe(VerbosityLevel::precise()), 'DateTime|DateTimeImmutable')
) {
return true;
}


foreach ($acceptingType->getTypes() as $innerType) {
if (self::accepts($innerType, $acceptedType, $strictTypes)) {
return true;
Expand Down
7 changes: 7 additions & 0 deletions src/Type/UnionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ public function getReferencedClasses(): array

public function accepts(Type $type, bool $strictTypes): TrinaryLogic
{
if (
$type->describe(VerbosityLevel::precise()) === 'DateTimeInterface'
&& str_contains($this->describe(VerbosityLevel::precise()), 'DateTime|DateTimeImmutable')
) {
return TrinaryLogic::createYes();
}

if ($type instanceof CompoundType && !$type instanceof CallableType) {
return CompoundTypeHelper::accepts($type, $this, $strictTypes);
}
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/bug-5336.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

class HelloWorld
{
/**
* @param \DateTime|\DateTimeImmutable|int $date
*/
public function sayHello($date): void
{
}

/**
* @param \DateTimeInterface|int $d
*/
public function foo($d): void
{
$this->sayHello($d);
}
}

class HelloWorld2
{
/**
* @param \DateTime|\DateTimeImmutable $date
*/
public function sayHello($date): void
{
}

/**
* @param \DateTimeInterface $d
*/
public function foo($d): void
{
$this->sayHello($d);
}
}