Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement require-extends rules #2859

Merged
merged 26 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c9fbe3
Implement IncompatibleRequireExtendsTypeRule
staabm Jan 7, 2024
ff66719
Implement RequireExtendsRule
staabm Jan 7, 2024
65dde9f
report final classes
staabm Jan 7, 2024
c6f272f
php <8.1 compat
staabm Jan 7, 2024
b535c9b
use ClassLike instead of InClassNode for phpdoc validation
staabm Jan 8, 2024
06d3343
implement require-implements rules
staabm Jan 8, 2024
a901c62
Update config.level2.neon
staabm Jan 8, 2024
fee7d1a
fix php 7.2 compat
staabm Jan 8, 2024
c44d540
fix name collision
staabm Jan 8, 2024
e6bcbe7
Rules that just have the tag should be in the rules section above.
staabm Jan 9, 2024
2295b93
Utilize ClassReflection->implementsInterface/isSubclassOf
staabm Jan 9, 2024
8d3d9d3
Use instanceof ObjectType
staabm Jan 9, 2024
619790d
cover anonymous classes in tests
staabm Jan 9, 2024
682c4a0
Work on InClassNode
staabm Jan 9, 2024
bb3ee3e
Separate InClassNode and InTraitNode rules
staabm Jan 9, 2024
7f7334e
Utilize ObjectType->getClassReflection()
staabm Jan 9, 2024
1374549
fix php 7.x expectations
staabm Jan 9, 2024
a1ed5d8
error on trait phpdocs only once per declaration
staabm Jan 9, 2024
131c5ca
Rename rule classes
ondrejmirtes Jan 10, 2024
df9cf28
Cosmetic change
ondrejmirtes Jan 10, 2024
fc125fe
Use Trait_
ondrejmirtes Jan 10, 2024
fdab25b
Improve error messages
ondrejmirtes Jan 10, 2024
ec3fc94
Support psalm prefix
staabm Jan 10, 2024
ea57e39
Refactoring: extract RequireExtendsCheck
staabm Jan 10, 2024
f7bda5c
use getDisplayName()
staabm Jan 10, 2024
e31c497
require-extends can only be used once
staabm Jan 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,29 @@ services:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Classes\RequireExtendsRule
tags:
- phpstan.rules.rule
staabm marked this conversation as resolved.
Show resolved Hide resolved
-
class: PHPStan\Rules\PhpDoc\IncompatibleRequireExtendsTypeRule
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Classes\RequireImplementsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\PhpDoc\IncompatibleRequireImplementsTypeRule
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule
-
Expand Down
76 changes: 76 additions & 0 deletions src/Rules/Classes/RequireExtendsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function in_array;
use function sprintf;

/**
* @implements Rule<InClassNode>
*/
class RequireExtendsRule implements Rule
{

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$parentNames = $classReflection->getParentClassesNames();

$errors = [];
foreach ($classReflection->getInterfaces() as $interface) {
staabm marked this conversation as resolved.
Show resolved Hide resolved
$extendsTags = $interface->getRequireExtendsTags();
foreach ($extendsTags as $extendsTag) {
$type = $extendsTag->getType();
$typeName = $type->describe(VerbosityLevel::typeOnly());

if (in_array($typeName, $parentNames, true)) {
staabm marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

$errors[] = RuleErrorBuilder::message(
sprintf(
'%s requires implementing class to extend %s, but %s does not.',
$interface->getName(),
$type->describe(VerbosityLevel::typeOnly()),
$classReflection->getName(),
),
)->build();
}
}

foreach ($classReflection->getTraits() as $trait) {
$extendsTags = $trait->getRequireExtendsTags();
foreach ($extendsTags as $extendsTag) {
$type = $extendsTag->getType();
$typeName = $type->describe(VerbosityLevel::typeOnly());

if (in_array($typeName, $parentNames, true)) {
staabm marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

$errors[] = RuleErrorBuilder::message(
sprintf(
'%s requires using class to extend %s, but %s does not.',
staabm marked this conversation as resolved.
Show resolved Hide resolved
$trait->getName(),
$type->describe(VerbosityLevel::typeOnly()),
$classReflection->getName(),
),
)->build();
}
}

return $errors;
}

}
57 changes: 57 additions & 0 deletions src/Rules/Classes/RequireImplementsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function array_map;
use function in_array;
use function sprintf;

/**
* @implements Rule<InClassNode>
*/
class RequireImplementsRule implements Rule
{

public function getNodeType(): string
{
return InClassNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$interfaceNames = array_map(static fn (ClassReflection $interface): string => $interface->getName(), $classReflection->getInterfaces());

$errors = [];
foreach ($classReflection->getTraits() as $trait) {
$implementsTags = $trait->getRequireImplementsTags();
foreach ($implementsTags as $implementsTag) {
$type = $implementsTag->getType();
$typeName = $type->describe(VerbosityLevel::typeOnly());

if (in_array($typeName, $interfaceNames, true)) {
staabm marked this conversation as resolved.
Show resolved Hide resolved
continue;
}

$errors[] = RuleErrorBuilder::message(
sprintf(
'%s requires using class to implement %s, but %s does not.',
$trait->getName(),
$type->describe(VerbosityLevel::typeOnly()),
$classReflection->getName(),
),
)->build();
}
}

return $errors;
}

}
125 changes: 125 additions & 0 deletions src/Rules/PhpDoc/IncompatibleRequireExtendsTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PhpParser\Node\Stmt\ClassLike;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassCaseSensitivityCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function count;
use function sprintf;

/**
* @implements Rule<ClassLike>
*/
class IncompatibleRequireExtendsTypeRule implements Rule
{

public function __construct(
private ReflectionProvider $reflectionProvider,
private ClassCaseSensitivityCheck $classCaseSensitivityCheck,
private UnresolvableTypeHelper $unresolvableTypeHelper,
private bool $checkClassCaseSensitivity,
)
{
}

public function getNodeType(): string
{
return ClassLike::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (
$node->namespacedName === null
|| !$this->reflectionProvider->hasClass($node->namespacedName->toString())
) {
return [];
}

$classReflection = $this->reflectionProvider->getClass($node->namespacedName->toString());
$extendsTags = $classReflection->getRequireExtendsTags();

if (
!$classReflection->isTrait()
&& ! $classReflection->isInterface()
staabm marked this conversation as resolved.
Show resolved Hide resolved
&& count($extendsTags) > 0
) {
return [
RuleErrorBuilder::message('PHPDoc tag @require-extends is only valid on trait or interface.')->build(),
];
}

$errors = [];
foreach ($extendsTags as $extendsTag) {
$type = $extendsTag->getType();
staabm marked this conversation as resolved.
Show resolved Hide resolved
if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))->build();
continue;
}

if (
$this->unresolvableTypeHelper->containsUnresolvableType($type)
) {
$errors[] = RuleErrorBuilder::message('PHPDoc tag @require-extends contains unresolvable type.')->build();
continue;
}

if (
$this->containsGenericType($type)
) {
$errors[] = RuleErrorBuilder::message('PHPDoc tag @require-extends cannot contain generic type.')->build();
continue;
}

foreach ($type->getReferencedClasses() as $class) {
if (!$this->reflectionProvider->hasClass($class)) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @require-extends contains unknown class %s.', $class))->discoveringSymbolsTip()->build();
continue;
}

$referencedClassReflection = $this->reflectionProvider->getClass($class);
if (!$referencedClassReflection->isClass()) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @require-extends cannot contain non-class type %s.', $class))->build();
} elseif ($referencedClassReflection->isFinal()) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @require-extends cannot contain final class %s.', $class))->build();
} elseif ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames([
new ClassNameNodePair($class, $node),
]),
);
}
}
}

return $errors;
}

private function containsGenericType(Type $phpDocType): bool
{
$containsGeneric = false;
TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$containsGeneric): Type {
if ($type instanceof GenericObjectType) {
$containsGeneric = true;
return $type;
}
$traverse($type);
return $type;
});

return $containsGeneric;
}

}