-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PHP 8.3 - check class constant PHPDoc type and native type against as…
…signed value
- Loading branch information
1 parent
8cd239e
commit 1a55bef
Showing
15 changed files
with
347 additions
and
92 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
133 changes: 133 additions & 0 deletions
133
src/Rules/Constants/ValueAssignedToClassConstantRule.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,133 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Constants; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Reflection\ClassConstantReflection; | ||
use PHPStan\Reflection\ClassReflection; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleError; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\ShouldNotHappenException; | ||
use PHPStan\Type\ParserNodeTypeToPHPStanType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\VerbosityLevel; | ||
use function array_merge; | ||
use function sprintf; | ||
|
||
/** | ||
* @implements Rule<Node\Stmt\ClassConst> | ||
*/ | ||
class ValueAssignedToClassConstantRule implements Rule | ||
{ | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node\Stmt\ClassConst::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$scope->isInClass()) { | ||
throw new ShouldNotHappenException(); | ||
} | ||
|
||
$nativeType = null; | ||
if ($node->type !== null) { | ||
$nativeType = ParserNodeTypeToPHPStanType::resolve($node->type, $scope->getClassReflection()); | ||
} | ||
|
||
$errors = []; | ||
foreach ($node->consts as $const) { | ||
$constantName = $const->name->toString(); | ||
$errors = array_merge($errors, $this->processSingleConstant( | ||
$scope->getClassReflection(), | ||
$constantName, | ||
$scope->getType($const->value), | ||
$nativeType, | ||
)); | ||
} | ||
|
||
return $errors; | ||
} | ||
|
||
/** | ||
* @return RuleError[] | ||
*/ | ||
private function processSingleConstant(ClassReflection $classReflection, string $constantName, Type $valueExprType, ?Type $nativeType): array | ||
{ | ||
$constantReflection = $classReflection->getConstant($constantName); | ||
if (!$constantReflection instanceof ClassConstantReflection) { | ||
return []; | ||
} | ||
|
||
$phpDocType = $constantReflection->getPhpDocType(); | ||
if ($phpDocType === null) { | ||
if ($nativeType === null) { | ||
return []; | ||
} | ||
|
||
$isSuperType = $nativeType->isSuperTypeOf($valueExprType); | ||
if ($isSuperType->yes()) { | ||
return []; | ||
} | ||
|
||
return [ | ||
RuleErrorBuilder::message(sprintf( | ||
'Constant %s::%s (%s) does not accept value %s.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName, | ||
$nativeType->describe(VerbosityLevel::typeOnly()), | ||
$valueExprType->describe(VerbosityLevel::value()), | ||
))->nonIgnorable()->build(), | ||
]; | ||
} elseif ($nativeType === null) { | ||
$isSuperType = $phpDocType->isSuperTypeOf($valueExprType); | ||
$verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $valueExprType); | ||
if ($isSuperType->no()) { | ||
return [ | ||
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), | ||
$valueExprType->describe(VerbosityLevel::value()), | ||
))->build(), | ||
]; | ||
|
||
} elseif ($isSuperType->maybe()) { | ||
return [ | ||
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), | ||
$valueExprType->describe(VerbosityLevel::value()), | ||
))->build(), | ||
]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
$type = $constantReflection->getValueType(); | ||
$isSuperType = $type->isSuperTypeOf($valueExprType); | ||
if ($isSuperType->yes()) { | ||
return []; | ||
} | ||
|
||
$verbosity = VerbosityLevel::getRecommendedLevelByType($type, $valueExprType); | ||
|
||
return [ | ||
RuleErrorBuilder::message(sprintf( | ||
'Constant %s::%s (%s) does not accept value %s.', | ||
$constantReflection->getDeclaringClass()->getDisplayName(), | ||
$constantName, | ||
$type->describe(VerbosityLevel::typeOnly()), | ||
$valueExprType->describe($verbosity), | ||
))->build(), | ||
]; | ||
} | ||
|
||
} |
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
81 changes: 81 additions & 0 deletions
81
tests/PHPStan/Rules/Constants/ValueAssignedToClassConstantRuleTest.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,81 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Constants; | ||
|
||
use PHPStan\Rules\Rule as TRule; | ||
use PHPStan\Testing\RuleTestCase; | ||
use const PHP_VERSION_ID; | ||
|
||
/** | ||
* @extends RuleTestCase<ValueAssignedToClassConstantRule> | ||
*/ | ||
class ValueAssignedToClassConstantRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): TRule | ||
{ | ||
return new ValueAssignedToClassConstantRule(); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/value-assigned-to-class-constant.php'], [ | ||
[ | ||
'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::BAZ with type string is incompatible with value 1.', | ||
14, | ||
], | ||
[ | ||
'PHPDoc tag @var for constant ValueAssignedToClassConstant\Foo::DOLOR with type ValueAssignedToClassConstant\Foo<int> is incompatible with value 1.', | ||
23, | ||
], | ||
[ | ||
'PHPDoc tag @var for constant ValueAssignedToClassConstant\Bar::BAZ with type string is incompatible with value 2.', | ||
32, | ||
], | ||
]); | ||
} | ||
|
||
public function testBug7352(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/bug-7352.php'], []); | ||
} | ||
|
||
public function testBug7352WithSubNamespace(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/bug-7352-with-sub-namespace.php'], []); | ||
} | ||
|
||
public function testBug7273(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/bug-7273.php'], []); | ||
} | ||
|
||
public function testBug7273b(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/bug-7273b.php'], []); | ||
} | ||
|
||
public function testBug5655(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/bug-5655.php'], []); | ||
} | ||
|
||
public function testNativeType(): void | ||
{ | ||
if (PHP_VERSION_ID < 80300) { | ||
$this->markTestSkipped('Test requires PHP 8.3.'); | ||
} | ||
|
||
$this->analyse([__DIR__ . '/data/value-assigned-to-class-constant-native-type.php'], [ | ||
[ | ||
'Constant ValueAssignedToClassConstantNativeType\Foo::BAR (int) does not accept value \'bar\'.', | ||
10, | ||
], | ||
[ | ||
'Constant ValueAssignedToClassConstantNativeType\Bar::BAR (int<1, max>) does not accept value 0.', | ||
21, | ||
], | ||
]); | ||
} | ||
|
||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
23 changes: 23 additions & 0 deletions
23
tests/PHPStan/Rules/Constants/data/value-assigned-to-class-constant-native-type.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,23 @@ | ||
<?php // lint >= 8.3 | ||
|
||
namespace ValueAssignedToClassConstantNativeType; | ||
|
||
class Foo | ||
{ | ||
|
||
public const int FOO = 1; | ||
|
||
public const int BAR = 'bar'; | ||
|
||
} | ||
|
||
class Bar | ||
{ | ||
|
||
/** @var int<1, max> */ | ||
public const int FOO = 1; | ||
|
||
/** @var int<1, max> */ | ||
public const int BAR = 0; | ||
|
||
} |
Oops, something went wrong.