Skip to content

Commit fed292e

Browse files
dpimglaman
andauthored
✨ Narrow return value of getKey by whether hasKey was called beforehand (#905)
✨ Narrow return value of getKey by whether hasKey was called beforehand Co-authored-by: Matt Glaman <[email protected]>
1 parent d043a25 commit fed292e

File tree

3 files changed

+76
-0
lines changed

3 files changed

+76
-0
lines changed

extension.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ services:
290290
-
291291
class: mglaman\PHPStanDrupal\Type\EntityIdNarrowedByNew
292292
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
293+
-
294+
class: mglaman\PHPStanDrupal\Type\EntityTypeKeyByExistence
295+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
293296
-
294297
class: mglaman\PHPStanDrupal\Type\EntityTypeLinkTemplateByExistence
295298
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace mglaman\PHPStanDrupal\Type;
4+
5+
use Drupal\Core\Entity\EntityTypeInterface;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PhpParser\Node\Identifier;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
12+
use PHPStan\Type\StringType;
13+
use PHPStan\Type\Type;
14+
15+
/**
16+
* @author Daniel Phin <[email protected]>
17+
*/
18+
class EntityTypeKeyByExistence implements DynamicMethodReturnTypeExtension
19+
{
20+
21+
public function getClass(): string
22+
{
23+
return EntityTypeInterface::class;
24+
}
25+
26+
public function isMethodSupported(MethodReflection $methodReflection): bool
27+
{
28+
return $methodReflection->getName() === 'getKey';
29+
}
30+
31+
public function getTypeFromMethodCall(
32+
MethodReflection $methodReflection,
33+
MethodCall $methodCall,
34+
Scope $scope
35+
): ?Type {
36+
$keyArg = $methodCall->getArg('key', 0);
37+
if (null === $keyArg) {
38+
// Key is a required arg but ignore if it isn't set.
39+
return null;
40+
}
41+
42+
$originalReturnType = $methodReflection->getVariants()[0]->getReturnType();
43+
$hasKeyMethodCall = new MethodCall($methodCall->var, new Identifier('hasKey'), [$keyArg]);
44+
if ($scope->getType($hasKeyMethodCall)->isTrue()->yes()) {
45+
return $originalReturnType->tryRemove(new ConstantBooleanType(false));
46+
} elseif ($scope->getType($hasKeyMethodCall)->isFalse()->yes()) {
47+
return $originalReturnType->tryRemove(new StringType());
48+
}
49+
50+
return $originalReturnType;
51+
}
52+
}

tests/src/Type/data/entity-type-stubs.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ function (ContentEntityInterface $entity): void
1717
}
1818
};
1919

20+
assert($entityTypeDefault instanceof EntityType);
21+
assertType('string|false', $entityTypeDefault->getKey('foo'));
22+
23+
assert($noKey instanceof EntityType);
24+
assert($noKey->hasKey('foo') === FALSE);
25+
assertType('false', $noKey->getKey('foo'));
26+
27+
assert($hasKey instanceof EntityType);
28+
assert($hasKey->hasKey('foo') === TRUE);
29+
assertType('string', $hasKey->getKey('foo'));
30+
31+
// Test getting a key doesn't affect another key.
32+
assert($entityType instanceof EntityType);
33+
assert($entityType->hasKey('foo') === TRUE);
34+
assertType('string', $entityType->getKey('foo'));
35+
// A different arg that wasn't narrowed previously:
36+
assertType('string|false', $entityType->getKey('bar'));
37+
// ...until we know better:
38+
assert($entityType->hasKey('bar') === TRUE);
39+
assertType('string', $entityType->getKey('bar'));
40+
2041
assert($entityTypeDefault instanceof EntityType);
2142
assertType('string|false', $entityTypeDefault->getLinkTemplate('foo'));
2243

0 commit comments

Comments
 (0)