Skip to content

Commit

Permalink
Do not report static in PHPDoc tags above traits as an error
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Sep 3, 2024
1 parent 9c4bee9 commit 777a82a
Show file tree
Hide file tree
Showing 30 changed files with 898 additions and 159 deletions.
5 changes: 5 additions & 0 deletions conf/config.level0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.validatePregQuote%
PHPStan\Rules\Keywords\RequireFileExistsRule:
phpstan.rules.rule: %featureToggles.requireFileExists%
PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%

rules:
- PHPStan\Rules\Api\ApiInstantiationRule
Expand Down Expand Up @@ -148,6 +150,9 @@ services:
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%

-
class: PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule

-
class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule
tags:
Expand Down
10 changes: 10 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\MethodTagTraitRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\MethodTagTraitUseRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\PropertyTagRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\PropertyTagTraitRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Classes\PropertyTagTraitUseRule:
phpstan.rules.rule: %featureToggles.absentTypeChecks%
PHPStan\Rules\Functions\IncompatibleArrowFunctionDefaultParameterTypeRule:
phpstan.rules.rule: %featureToggles.closureDefaultParameterTypeRule%
PHPStan\Rules\Functions\IncompatibleClosureDefaultParameterTypeRule:
Expand Down Expand Up @@ -89,12 +93,18 @@ services:
-
class: PHPStan\Rules\Classes\MethodTagTraitRule

-
class: PHPStan\Rules\Classes\MethodTagTraitUseRule

-
class: PHPStan\Rules\Classes\PropertyTagRule

-
class: PHPStan\Rules\Classes\PropertyTagTraitRule

-
class: PHPStan\Rules\Classes\PropertyTagTraitUseRule

-
class: PHPStan\Rules\PhpDoc\RequireExtendsCheck
arguments:
Expand Down
5 changes: 4 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5788,8 +5788,11 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection
$methodAst->name = $methodNames[$methodName];
}

if (!$scope->isInClass()) {
throw new ShouldNotHappenException();
}
$traitScope = $scope->enterTrait($traitReflection);
$nodeCallback(new InTraitNode($node, $traitReflection), $traitScope);
$nodeCallback(new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope);
$this->processStmtNodes($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel());
return;
}
Expand Down
7 changes: 6 additions & 1 deletion src/Node/InTraitNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class InTraitNode extends Node\Stmt implements VirtualNode
{

public function __construct(private Node\Stmt\Trait_ $originalNode, private ClassReflection $traitReflection)
public function __construct(private Node\Stmt\Trait_ $originalNode, private ClassReflection $traitReflection, private ClassReflection $implementingClassReflection)
{
parent::__construct($originalNode->getAttributes());
}
Expand All @@ -27,6 +27,11 @@ public function getTraitReflection(): ClassReflection
return $this->traitReflection;
}

public function getImplementingClassReflection(): ClassReflection
{
return $this->implementingClassReflection;
}

public function getType(): string
{
return 'PHPStan_Stmt_InTraitNode';
Expand Down
6 changes: 6 additions & 0 deletions src/PhpDoc/StubValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@
use PHPStan\Rules\Classes\LocalTypeAliasesCheck;
use PHPStan\Rules\Classes\LocalTypeAliasesRule;
use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule;
use PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule;
use PHPStan\Rules\Classes\MethodTagCheck;
use PHPStan\Rules\Classes\MethodTagRule;
use PHPStan\Rules\Classes\MethodTagTraitRule;
use PHPStan\Rules\Classes\MethodTagTraitUseRule;
use PHPStan\Rules\Classes\MixinRule;
use PHPStan\Rules\Classes\PropertyTagCheck;
use PHPStan\Rules\Classes\PropertyTagRule;
use PHPStan\Rules\Classes\PropertyTagTraitRule;
use PHPStan\Rules\Classes\PropertyTagTraitUseRule;
use PHPStan\Rules\ClassNameCheck;
use PHPStan\Rules\DirectRegistry as DirectRuleRegistry;
use PHPStan\Rules\FunctionDefinitionCheck;
Expand Down Expand Up @@ -242,11 +245,14 @@ private function getRuleRegistry(Container $container): RuleRegistry
$methodTagCheck = new MethodTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
$rules[] = new MethodTagRule($methodTagCheck);
$rules[] = new MethodTagTraitRule($methodTagCheck, $reflectionProvider);
$rules[] = new MethodTagTraitUseRule($methodTagCheck);

$propertyTagCheck = new PropertyTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
$rules[] = new PropertyTagRule($propertyTagCheck);
$rules[] = new PropertyTagTraitRule($propertyTagCheck, $reflectionProvider);
$rules[] = new PropertyTagTraitUseRule($propertyTagCheck);
$rules[] = new MixinRule($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true, true);
$rules[] = new LocalTypeTraitUseAliasesRule($localTypeAliasesCheck);
}

return new DirectRuleRegistry($rules);
Expand Down
27 changes: 27 additions & 0 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ class ClassReflection

private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false;

private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false;

/** @var ClassReflection[]|null */
private ?array $cachedInterfaces = null;

Expand Down Expand Up @@ -1580,6 +1582,31 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment);
}

public function getTraitContextResolvedPhpDoc(self $implementingClass): ?ResolvedPhpDocBlock
{
if (!$this->isTrait()) {
throw new ShouldNotHappenException();
}
if (!$implementingClass->isClass()) {
throw new ShouldNotHappenException();
}
$fileName = $this->getFileName();
if (is_bool($this->reflectionDocComment)) {
$docComment = $this->reflection->getDocComment();
$this->reflectionDocComment = $docComment !== false ? $docComment : null;
}

if ($this->reflectionDocComment === null) {
return null;
}

if ($this->traitContextResolvedPhpDocBlock !== false) {
return $this->traitContextResolvedPhpDocBlock;
}

return $this->traitContextResolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $implementingClass->getName(), $this->getName(), null, $this->reflectionDocComment);
}

private function getFirstExtendsTag(): ?ExtendsTag
{
foreach ($this->getExtendsTags() as $tag) {
Expand Down
182 changes: 120 additions & 62 deletions src/Rules/Classes/LocalTypeAliasesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ public function __construct(
* @return list<IdentifierRuleError>
*/
public function check(ClassReflection $reflection, ClassLike $node): array
{
$errors = [];
foreach ($this->checkInTraitDefinitionContext($reflection) as $error) {
$errors[] = $error;
}
foreach ($this->checkInTraitUseContext($reflection, $reflection, $node) as $error) {
$errors[] = $error;
}

return $errors;
}

/**
* @return list<IdentifierRuleError>
*/
public function checkInTraitDefinitionContext(ClassReflection $reflection): array
{
$phpDoc = $reflection->getResolvedPhpDoc();
if ($phpDoc === null) {
Expand All @@ -69,7 +85,7 @@ public function check(ClassReflection $reflection, ClassLike $node): array
};

$errors = [];
$className = $reflection->getName();
$className = $reflection->getDisplayName();

$importedAliases = [];

Expand Down Expand Up @@ -162,78 +178,86 @@ public function check(ClassReflection $reflection, ClassLike $node): array
}

$resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver);
$foundError = false;
TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
if ($foundError) {
return $type;
}

if ($type instanceof CircularTypeAliasErrorType) {
$errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))
->identifier('typeAlias.circular')
->build();
$foundError = true;
return $type;
}

if ($type instanceof ErrorType) {
$errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))
->identifier('typeAlias.invalidType')
->build();
$foundError = true;
return $type;
}

return $traverse($type);
});

if ($foundError) {
if ($this->hasErrorType($resolvedType, $aliasName, $errors)) {
continue;
}

if (!$this->absentTypeChecks) {
continue;
}

if ($this->checkMissingTypehints) {
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($resolvedType) as $iterableType) {
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with no value type specified in iterable type %s.',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$iterableTypeDescription,
))
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
->identifier('missingType.iterableValue')
->build();
}
if (!$this->checkMissingTypehints) {
continue;
}

foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($resolvedType) as [$name, $genericTypeNames]) {
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with generic %s but does not specify its types: %s',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$name,
implode(', ', $genericTypeNames),
))
->identifier('missingType.generics')
->build();
}
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($resolvedType) as $iterableType) {
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with no value type specified in iterable type %s.',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$iterableTypeDescription,
))
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
->identifier('missingType.iterableValue')
->build();
}

foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($resolvedType) as $callableType) {
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with no signature specified for %s.',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$callableType->describe(VerbosityLevel::typeOnly()),
))->identifier('missingType.callable')->build();
}
foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($resolvedType) as [$name, $genericTypeNames]) {
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with generic %s but does not specify its types: %s',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$name,
implode(', ', $genericTypeNames),
))
->identifier('missingType.generics')
->build();
}

foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($resolvedType) as $callableType) {
$errors[] = RuleErrorBuilder::message(sprintf(
'%s %s has type alias %s with no signature specified for %s.',
$reflection->getClassTypeDescription(),
$reflection->getDisplayName(),
$aliasName,
$callableType->describe(VerbosityLevel::typeOnly()),
))->identifier('missingType.callable')->build();
}
}

return $errors;
}

/**
* @return list<IdentifierRuleError>
*/
public function checkInTraitUseContext(
ClassReflection $reflection,
ClassReflection $implementingClassReflection,
ClassLike $node,
): array
{
if ($reflection->getNativeReflection()->getName() === $implementingClassReflection->getName()) {
$phpDoc = $reflection->getResolvedPhpDoc();
} else {
$phpDoc = $reflection->getTraitContextResolvedPhpDoc($implementingClassReflection);
}
if ($phpDoc === null) {
return [];
}

$errors = [];

foreach ($phpDoc->getTypeAliasTags() as $typeAliasTag) {
$aliasName = $typeAliasTag->getAliasName();
$resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver);
$throwawayErrors = [];
if ($this->hasErrorType($resolvedType, $aliasName, $throwawayErrors)) {
continue;
}
foreach ($resolvedType->getReferencedClasses() as $class) {
if (!$this->reflectionProvider->hasClass($class)) {
$errors[] = RuleErrorBuilder::message(sprintf('Type alias %s contains unknown class %s.', $aliasName, $class))
Expand Down Expand Up @@ -304,4 +328,38 @@ private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): boo
|| $aliasNameResolvedType instanceof TemplateType; // aliases take precedence over type parameters, this is reported by other rules using TemplateTypeCheck
}

/**
* @param list<IdentifierRuleError> $errors
* @param-out list<IdentifierRuleError> $errors
*/
private function hasErrorType(Type $type, string $aliasName, array &$errors): bool
{
$foundError = false;
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
if ($foundError) {
return $type;
}

if ($type instanceof CircularTypeAliasErrorType) {
$errors[] = RuleErrorBuilder::message(sprintf('Circular definition detected in type alias %s.', $aliasName))
->identifier('typeAlias.circular')
->build();
$foundError = true;
return $type;
}

if ($type instanceof ErrorType) {
$errors[] = RuleErrorBuilder::message(sprintf('Invalid type definition detected in type alias %s.', $aliasName))
->identifier('typeAlias.invalidType')
->build();
$foundError = true;
return $type;
}

return $traverse($type);
});

return $foundError;
}

}
2 changes: 1 addition & 1 deletion src/Rules/Classes/LocalTypeTraitAliasesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

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

}
Loading

0 comments on commit 777a82a

Please sign in to comment.