-
Notifications
You must be signed in to change notification settings - Fork 471
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bleeding edge - IncompatibleClassConstantPhpDocTypeRule
- Loading branch information
1 parent
4cb02d1
commit 780a54c
Showing
8 changed files
with
212 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\PhpDoc; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\ClassConstantReflection; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Rules\Generics\GenericObjectTypeCheck; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\ConstantTypeHelper; | ||
use PHPStan\Type\VerbosityLevel; | ||
|
||
/** | ||
* @implements \PHPStan\Rules\Rule<Node\Stmt\ClassConst> | ||
*/ | ||
class IncompatibleClassConstantPhpDocTypeRule implements Rule | ||
{ | ||
|
||
private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; | ||
|
||
private UnresolvableTypeHelper $unresolvableTypeHelper; | ||
|
||
public function __construct( | ||
GenericObjectTypeCheck $genericObjectTypeCheck, | ||
UnresolvableTypeHelper $unresolvableTypeHelper | ||
) | ||
{ | ||
$this->genericObjectTypeCheck = $genericObjectTypeCheck; | ||
$this->unresolvableTypeHelper = $unresolvableTypeHelper; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node\Stmt\ClassConst::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$scope->isInClass()) { | ||
throw new \PHPStan\ShouldNotHappenException(); | ||
} | ||
|
||
$errors = []; | ||
foreach ($node->consts as $const) { | ||
$constantName = $const->name->toString(); | ||
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
/** | ||
* @param string $constantName | ||
* @return RuleError[] | ||
*/ | ||
private function processSingleConstant(ClassReflection $classReflection, string $constantName): array | ||
{ | ||
$constantReflection = $classReflection->getConstant($constantName); | ||
if (!$constantReflection instanceof ClassConstantReflection) { | ||
return []; | ||
} | ||
|
||
if (!$constantReflection->hasPhpDocType()) { | ||
return []; | ||
} | ||
|
||
$phpDocType = $constantReflection->getValueType(); | ||
|
||
$errors = []; | ||
if ( | ||
$this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) | ||
) { | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'PHPDoc tag @var for constant %s::%s contains unresolvable type.', | ||
$constantReflection->getDeclaringClass()->getName(), | ||
$constantName | ||
))->build(); | ||
} else { | ||
$nativeType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); | ||
$isSuperType = $phpDocType->isSuperTypeOf($nativeType); | ||
$verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType); | ||
if ($isSuperType->no()) { | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName, | ||
$phpDocType->describe($verbosity), | ||
$nativeType->describe(VerbosityLevel::value()) | ||
))->build(); | ||
|
||
} elseif ($isSuperType->maybe()) { | ||
$errors[] = RuleErrorBuilder::message(sprintf( | ||
'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName, | ||
$phpDocType->describe($verbosity), | ||
$nativeType->describe(VerbosityLevel::value()) | ||
))->build(); | ||
} | ||
} | ||
|
||
return array_merge($errors, $this->genericObjectTypeCheck->check( | ||
$phpDocType, | ||
sprintf( | ||
'PHPDoc tag @var for constant %s::%s contains generic type %%s but class %%s is not generic.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName | ||
), | ||
sprintf( | ||
'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of class %%s: %%s', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName | ||
), | ||
sprintf( | ||
'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but class %%s supports only %%d: %%s', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName | ||
), | ||
sprintf( | ||
'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of class %%s.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName | ||
) | ||
)); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\PhpDoc; | ||
|
||
use PHPStan\Rules\Generics\GenericObjectTypeCheck; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
|
||
/** | ||
* @extends RuleTestCase<IncompatibleClassConstantPhpDocTypeRule> | ||
*/ | ||
class IncompatibleClassConstantPhpDocTypeRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper(true)); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc.php'], [ | ||
[ | ||
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::FOO contains unresolvable type.', | ||
9, | ||
], | ||
[ | ||
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::BAZ with type string is incompatible with value 1.', | ||
17, | ||
], | ||
[ | ||
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR with type IncompatibleClassConstantPhpDoc\Foo<int> is incompatible with value 1.', | ||
26, | ||
], | ||
[ | ||
'PHPDoc tag @var for constant IncompatibleClassConstantPhpDoc\Foo::DOLOR contains generic type IncompatibleClassConstantPhpDoc\Foo<int> but class IncompatibleClassConstantPhpDoc\Foo is not generic.', | ||
26, | ||
], | ||
]); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
|
||
namespace IncompatibleClassConstantPhpDoc; | ||
|
||
class Foo | ||
{ | ||
|
||
/** @var self&\stdClass */ | ||
const FOO = 1; | ||
|
||
/** @var int */ | ||
const BAR = 1; | ||
|
||
const NO_TYPE = 'string'; | ||
|
||
/** @var string */ | ||
const BAZ = 1; | ||
|
||
/** @var string|int */ | ||
const LOREM = 1; | ||
|
||
/** @var int */ | ||
const IPSUM = self::LOREM; // resolved to 1, I'd prefer string|int | ||
|
||
/** @var self<int> */ | ||
const DOLOR = 1; | ||
|
||
} |