Skip to content

Commit

Permalink
Bleeding edge - IncompatibleClassConstantPhpDocTypeRule
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 17, 2021
1 parent 4cb02d1 commit 780a54c
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 5 deletions.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ parameters:
validateOverridingMethodsInStubs: true
crossCheckInterfaces: true
finalByPhpDocTag: true
classConstants: true
stubFiles:
- ../stubs/arrayFunctions.stub
6 changes: 6 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ parameters:
checkThisOnly: false
checkPhpDocMissingReturn: true

conditionalTags:
PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule:
phpstan.rules.rule: %featureToggles.classConstants%

rules:
- PHPStan\Rules\Cast\EchoRule
- PHPStan\Rules\Cast\InvalidCastRule
Expand Down Expand Up @@ -50,6 +54,8 @@ services:
crossCheckInterfaces: %featureToggles.crossCheckInterfaces%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule
-
class: PHPStan\Rules\Generics\InterfaceAncestorsRule
arguments:
Expand Down
4 changes: 3 additions & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ parameters:
validateOverridingMethodsInStubs: false
crossCheckInterfaces: false
finalByPhpDocTag: false
classConstants: false
fileExtensions:
- php
checkAlwaysTrueCheckTypeFunctionCall: false
Expand Down Expand Up @@ -225,7 +226,8 @@ parametersSchema:
neverInGenericReturnType: bool(),
validateOverridingMethodsInStubs: bool(),
crossCheckInterfaces: bool(),
finalByPhpDocTag: bool()
finalByPhpDocTag: bool(),
classConstants: bool()
])
fileExtensions: listOf(string())
checkAlwaysTrueCheckTypeFunctionCall: bool()
Expand Down
130 changes: 130 additions & 0 deletions src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php
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
)
));
}

}
2 changes: 2 additions & 0 deletions src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public function processNode(Node $node, Scope $scope): array
if (
$node instanceof Node\Stmt\Property
|| $node instanceof Node\Stmt\PropertyProperty
|| $node instanceof Node\Stmt\ClassConst
|| $node instanceof Node\Stmt\Const_
) {
return [];
}
Expand Down
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,
],
]);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,6 @@ public function testRule(): void
67,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'PHPDoc tag @var contains unresolvable type.',
90,
],
]);
}

Expand Down
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;

}

0 comments on commit 780a54c

Please sign in to comment.