Skip to content

Commit

Permalink
Bleeding edge - check types in @method tags
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 26, 2024
1 parent 3e51899 commit 5b7e474
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 6 deletions.
10 changes: 10 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ rules:
- PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule

conditionalTags:
PHPStan\Rules\Classes\MethodTagRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\MethodTagTraitRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\PropertyTagRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\PropertyTagTraitRule:
Expand Down Expand Up @@ -79,6 +83,12 @@ services:
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Classes\MethodTagRule

-
class: PHPStan\Rules\Classes\MethodTagTraitRule

-
class: PHPStan\Rules\Classes\PropertyTagRule

Expand Down
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,11 @@ services:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
absentTypeChecks: %featureToggles.absentTypeChecks%

-
class: PHPStan\Rules\Classes\MethodTagCheck
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%

-
class: PHPStan\Rules\Classes\PropertyTagCheck
arguments:
Expand Down
7 changes: 7 additions & 0 deletions src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
use PHPStan\Rules\Classes\LocalTypeAliasesCheck;
use PHPStan\Rules\Classes\LocalTypeAliasesRule;
use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule;
use PHPStan\Rules\Classes\MethodTagCheck;
use PHPStan\Rules\Classes\MethodTagRule;
use PHPStan\Rules\Classes\MethodTagTraitRule;
use PHPStan\Rules\Classes\MixinRule;
use PHPStan\Rules\Classes\PropertyTagCheck;
use PHPStan\Rules\Classes\PropertyTagRule;
Expand Down Expand Up @@ -236,6 +239,10 @@ private function getRuleRegistry(Container $container): RuleRegistry
if ((bool) $container->getParameter('featureToggles')['absentTypeChecks']) {
$rules[] = new MissingMethodSelfOutTypeRule($missingTypehintCheck);

$methodTagCheck = new MethodTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
$rules[] = new MethodTagRule($methodTagCheck);
$rules[] = new MethodTagTraitRule($methodTagCheck, $reflectionProvider);

$propertyTagCheck = new PropertyTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
$rules[] = new PropertyTagRule($propertyTagCheck);
$rules[] = new PropertyTagTraitRule($propertyTagCheck, $reflectionProvider);
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1745,7 +1745,7 @@ public function getPropertyTags(): array
}

/**
* @return array<MethodTag>
* @return array<string, MethodTag>
*/
public function getMethodTags(): array
{
Expand Down
164 changes: 164 additions & 0 deletions src/Rules/Classes/MethodTagCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node\Stmt\ClassLike;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\MissingTypehintCheck;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function implode;
use function sprintf;

final class MethodTagCheck
{

public function __construct(
private ReflectionProvider $reflectionProvider,
private ClassNameCheck $classCheck,
private GenericObjectTypeCheck $genericObjectTypeCheck,
private MissingTypehintCheck $missingTypehintCheck,
private UnresolvableTypeHelper $unresolvableTypeHelper,
private bool $checkClassCaseSensitivity,
)
{
}

/**
* @return list<IdentifierRuleError>
*/
public function check(
ClassReflection $classReflection,
ClassLike $node,
): array
{
$errors = [];
foreach ($classReflection->getMethodTags() as $methodName => $methodTag) {
$i = 0;
foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
$i++;
$parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName);
foreach ($this->checkMethodType($classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) {
$errors[] = $error;
}

if ($parameterTag->getDefaultValue() === null) {
continue;
}

foreach ($this->checkMethodType($classReflection, $methodName, sprintf('%s default value', $parameterDescription), $parameterTag->getDefaultValue(), $node) as $error) {
$errors[] = $error;
}
}

foreach ($this->checkMethodType($classReflection, $methodName, 'return type', $methodTag->getReturnType(), $node) as $error) {
$errors[] = $error;
}
}

return $errors;
}

/**
* @return list<IdentifierRuleError>
*/
private function checkMethodType(ClassReflection $classReflection, string $methodName, string $description, Type $type, ClassLike $node): array
{
if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) {
return [
RuleErrorBuilder::message(sprintf(
'PHPDoc tag @method for method %s::%s() %s contains unresolvable type.',
$classReflection->getDisplayName(),
$methodName,
$description,
))->identifier('methodTag.unresolvableType')
->build(),
];
}

$escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName());
$escapedMethodName = SprintfHelper::escapeFormatString($methodName);
$escapedDescription = SprintfHelper::escapeFormatString($description);

$errors = $this->genericObjectTypeCheck->check(
$type,
sprintf('PHPDoc tag @method for method %s::%s() %s contains generic type %%s but %%s %%s is not generic.', $escapedClassName, $escapedMethodName, $escapedDescription),
sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s does not specify all template types of %%s %%s: %%s', $escapedClassName, $escapedMethodName, $escapedDescription),
sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedClassName, $escapedMethodName, $escapedDescription),
sprintf('Type %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is not subtype of template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription),
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is in conflict with %%s template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription),
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedClassName, $escapedMethodName, $escapedDescription),
);

foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) {
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @method for method %s::%s() %s contains generic %s but does not specify its types: %s',
$classReflection->getDisplayName(),
$methodName,
$description,
$innerName,
implode(', ', $genericTypeNames),
))
->identifier('missingType.generics')
->build();
}

foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) {
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has PHPDoc tag @method for method %s() %s with no value type specified in iterable type %s.',
$classReflection->getClassTypeDescription(),
$classReflection->getDisplayName(),
$methodName,
$description,
$iterableTypeDescription,
))
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
->identifier('missingType.iterableValue')
->build();
}

foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) {
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has PHPDoc tag @method for method %s() %s with no signature specified for %s.',
$classReflection->getClassTypeDescription(),
$classReflection->getDisplayName(),
$methodName,
$description,
$callableType->describe(VerbosityLevel::typeOnly()),
))->identifier('missingType.callable')->build();
}

foreach ($type->getReferencedClasses() as $class) {
if (!$this->reflectionProvider->hasClass($class)) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains unknown class %s.', $classReflection->getDisplayName(), $methodName, $description, $class))
->identifier('class.notFound')
->discoveringSymbolsTip()
->build();
} elseif ($this->reflectionProvider->getClass($class)->isTrait()) {
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains invalid type %s.', $classReflection->getDisplayName(), $methodName, $description, $class))
->identifier('methodTag.trait')
->build();
} else {
$errors = array_merge(
$errors,
$this->classCheck->checkClassNames([
new ClassNameNodePair($class, $node),
], $this->checkClassCaseSensitivity),
);
}
}

return $errors;
}

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

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;

/**
* @implements Rule<InClassNode>
*/
final class MethodTagRule implements Rule
{

public function __construct(private MethodTagCheck $check)
{
}

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

public function processNode(Node $node, Scope $scope): array
{
return $this->check->check($node->getClassReflection(), $node->getOriginalNode());
}

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

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;

/**
* @implements Rule<Node\Stmt\Trait_>
*/
final class MethodTagTraitRule implements Rule
{

public function __construct(private MethodTagCheck $check, private ReflectionProvider $reflectionProvider)
{
}

public function getNodeType(): string
{
return Node\Stmt\Trait_::class;
}

public function processNode(Node $node, Scope $scope): array
{
$traitName = $node->namespacedName;
if ($traitName === null) {
return [];
}

if (!$this->reflectionProvider->hasClass($traitName->toString())) {
return [];
}

return $this->check->check($this->reflectionProvider->getClass($traitName->toString()), $node);
}

}
Loading

0 comments on commit 5b7e474

Please sign in to comment.