Skip to content

Commit

Permalink
implement require-implements
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Jan 5, 2024
1 parent 65170f9 commit 7295e4a
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PHPStan\PhpDoc\Tag\ParamTag;
use PHPStan\PhpDoc\Tag\PropertyTag;
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
Expand All @@ -27,6 +28,7 @@
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\RequireImplementsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
Expand Down Expand Up @@ -421,6 +423,16 @@ public function resolveRequireExtendsTags(PhpDocNode $phpDocNode, NameScope $nam
), $phpDocNode->getRequireExtendsTagValues());
}

/**
* @return array<RequireImplementsTag>
*/
public function resolveRequireImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
return array_map(fn (RequireImplementsTagValueNode $requireImplementsTagValueNode): RequireImplementsTag => new RequireImplementsTag(
$this->typeNodeResolver->resolve($requireImplementsTagValueNode->type, $nameScope),
), $phpDocNode->getRequireImplementsTagValues());
}

/**
* @return array<string, TypeAliasTag>
*/
Expand Down
18 changes: 18 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\PhpDoc\Tag\ParamTag;
use PHPStan\PhpDoc\Tag\PropertyTag;
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\ReturnTag;
use PHPStan\PhpDoc\Tag\SelfOutTypeTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
Expand Down Expand Up @@ -97,6 +98,8 @@ class ResolvedPhpDocBlock

/** @var array<RequireExtendsTag>|false */
private array|false $requireExtendsTags = false;
/** @var array<RequireImplementsTag>|false */
private array|false $requireImplementsTags = false;

/** @var array<TypeAliasTag>|false */
private array|false $typeAliasTags = false;
Expand Down Expand Up @@ -559,6 +562,21 @@ public function getRequireExtendsTags(): array
return $this->requireExtendsTags;
}

/**
* @return array<RequireImplementsTag>
*/
public function getRequireImplementsTags(): array
{
if ($this->requireImplementsTags === false) {
$this->requireImplementsTags = $this->phpDocNodeResolver->resolveRequireImplementsTags(
$this->phpDocNode,
$this->getNameScope(),
);
}

return $this->requireImplementsTags;
}

/**
* @return array<TypeAliasTag>
*/
Expand Down
20 changes: 20 additions & 0 deletions src/PhpDoc/Tag/RequireImplementsTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDoc\Tag;

use PHPStan\Type\Type;

/** @api */
class RequireImplementsTag
{

public function __construct(private Type $type)
{
}

public function getType(): Type
{
return $this->type;
}

}
14 changes: 14 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PHPStan\PhpDoc\Tag\MixinTag;
use PHPStan\PhpDoc\Tag\PropertyTag;
use PHPStan\PhpDoc\Tag\RequireExtendsTag;
use PHPStan\PhpDoc\Tag\RequireImplementsTag;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
use PHPStan\PhpDoc\Tag\TypeAliasTag;
Expand Down Expand Up @@ -1611,6 +1612,19 @@ public function getRequireExtendsTags(): array
return $resolvedPhpDoc->getRequireExtendsTags();
}

/**
* @return array<RequireImplementsTag>
*/
public function getRequireImplementsTags(): array
{
$resolvedPhpDoc = $this->getResolvedPhpDoc();
if ($resolvedPhpDoc === null) {
return [];
}

return $resolvedPhpDoc->getRequireImplementsTags();
}

/**
* @return array<PropertyTag>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public function getMethod(ClassReflection $classReflection, string $methodName):

private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
{
$requireExtendsTags = $classReflection->getRequireExtendsTags();
foreach ($requireExtendsTags as $requireExtendsTag) {
$type = $requireExtendsTag->getType();
$extendsOrImplementsTags = array_merge($classReflection->getRequireExtendsTags(), $classReflection->getRequireImplementsTags());
foreach ($extendsOrImplementsTags as $extendsOrImplementsTag) {
$type = $extendsOrImplementsTag->getType();

$typeDescription = $type->describe(VerbosityLevel::typeOnly());
if (isset($this->inProcess[$typeDescription][$methodName])) {
Expand All @@ -52,7 +52,9 @@ private function findMethod(ClassReflection $classReflection, string $methodName

unset($this->inProcess[$typeDescription][$methodName]);

// XXX require-implements cannot grant access to static methods?
$static = $method->isStatic();

if (
!$static
&& $classReflection->hasNativeMethod('__callStatic')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa

private function findProperty(ClassReflection $classReflection, string $propertyName): ?PropertyReflection
{
// XXX require-implements cannot grant access to properties?
$requireExtendsTags = $classReflection->getRequireExtendsTags();
foreach ($requireExtendsTags as $requireExtendsTag) {
$type = $requireExtendsTag->getType();
Expand Down
15 changes: 15 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3147,4 +3147,19 @@ public function testRequireExtends(): void
]);
}

public function testRequireImplements(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->checkExplicitMixed = true;

$this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [
[
'Call to an undefined method RequireImplements\MyInterface::doesNotExist().',
38,
],
]);
}

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

public function testRequireImplements(): void
{
$this->checkThisOnly = false;
$this->checkExplicitMixed = false;

$this->analyse([__DIR__ . '/../Properties/data/require-implements.php'], [
[
'Call to an undefined static method RequireImplements\MyInterface::doesNotExistStatic().',
39,
],
]);
}

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

namespace RequireImplements;

interface MyInterface
{
public function doSomething(): string;

static public function doSomethingStatic(): int;
}

/**
* @phpstan-require-implements MyInterface
*/
trait MyTrait
{
public string $foo = 'hello';
}

abstract class MyBaseClass
{
use MyTrait;
}

function getFoo(MyBaseClass $obj): string
{
echo $obj->bar;
return $obj->foo;
}

function callFoo(MyBaseClass $obj): string
{
echo $obj->doesNotExist();
echo $obj::doesNotExistStatic();
echo $obj::doSomethingStatic();
return $obj->doSomething();
}

0 comments on commit 7295e4a

Please sign in to comment.