Skip to content

Commit

Permalink
PHP 8.3 - check class constant PHPDoc type and native type against as…
Browse files Browse the repository at this point in the history
…signed value
  • Loading branch information
ondrejmirtes committed Nov 6, 2023
1 parent 8cd239e commit 1a55bef
Show file tree
Hide file tree
Showing 15 changed files with 347 additions and 92 deletions.
1 change: 1 addition & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rules:
- PHPStan\Rules\Cast\PrintRule
- PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule
- PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule
- PHPStan\Rules\Constants\ValueAssignedToClassConstantRule
- PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule
- PHPStan\Rules\Generics\ClassAncestorsRule
- PHPStan\Rules\Generics\ClassTemplateTypeRule
Expand Down
5 changes: 5 additions & 0 deletions src/Reflection/ClassConstantReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ public function hasPhpDocType(): bool
return $this->phpDocType !== null;
}

public function getPhpDocType(): ?Type
{
return $this->phpDocType;
}

public function hasNativeType(): bool
{
return $this->nativeType !== null;
Expand Down
133 changes: 133 additions & 0 deletions src/Rules/Constants/ValueAssignedToClassConstantRule.php
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(),
];
}

}
37 changes: 19 additions & 18 deletions src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ClassConstantReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\InitializerExprContext;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
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;
Expand All @@ -27,7 +27,6 @@ class IncompatibleClassConstantPhpDocTypeRule implements Rule
public function __construct(
private GenericObjectTypeCheck $genericObjectTypeCheck,
private UnresolvableTypeHelper $unresolvableTypeHelper,
private InitializerExprTypeResolver $initializerExprTypeResolver,
)
{
}
Expand All @@ -43,10 +42,15 @@ public function processNode(Node $node, Scope $scope): array
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));
$errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $nativeType, $constantName));
}

return $errors;
Expand All @@ -55,19 +59,18 @@ public function processNode(Node $node, Scope $scope): array
/**
* @return RuleError[]
*/
private function processSingleConstant(ClassReflection $classReflection, string $constantName): array
private function processSingleConstant(ClassReflection $classReflection, ?Type $nativeType, string $constantName): array
{
$constantReflection = $classReflection->getConstant($constantName);
if (!$constantReflection instanceof ClassConstantReflection) {
return [];
}

if (!$constantReflection->hasPhpDocType()) {
$phpDocType = $constantReflection->getPhpDocType();
if ($phpDocType === null) {
return [];
}

$phpDocType = $constantReflection->getValueType();

$errors = [];
if (
$this->unresolvableTypeHelper->containsUnresolvableType($phpDocType)
Expand All @@ -77,26 +80,24 @@ private function processSingleConstant(ClassReflection $classReflection, string
$constantReflection->getDeclaringClass()->getName(),
$constantName,
))->build();
} else {
$nativeType = $this->initializerExprTypeResolver->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass()));
$isSuperType = $phpDocType->isSuperTypeOf($nativeType);
$verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType);
} elseif ($nativeType !== null) {
$isSuperType = $nativeType->isSuperTypeOf($phpDocType);
if ($isSuperType->no()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @var for constant %s::%s with type %s is incompatible with value %s.',
'PHPDoc tag @var for constant %s::%s with type %s is incompatible with native type %s.',
$constantReflection->getDeclaringClass()->getDisplayName(),
$constantName,
$phpDocType->describe($verbosity),
$nativeType->describe(VerbosityLevel::value()),
$phpDocType->describe(VerbosityLevel::typeOnly()),
$nativeType->describe(VerbosityLevel::typeOnly()),
))->build();

} elseif ($isSuperType->maybe()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @var for constant %s::%s with type %s is not subtype of value %s.',
'PHPDoc tag @var for constant %s::%s with type %s is not subtype of native type %s.',
$constantReflection->getDeclaringClass()->getDisplayName(),
$constantName,
$phpDocType->describe($verbosity),
$nativeType->describe(VerbosityLevel::value()),
$phpDocType->describe(VerbosityLevel::typeOnly()),
$nativeType->describe(VerbosityLevel::typeOnly()),
))->build();
}
}
Expand Down
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,
],
]);
}

}
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;

}
Loading

0 comments on commit 1a55bef

Please sign in to comment.