From 777a82a0dc9d6a64a709c30a2e5bdb030b634464 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 3 Sep 2024 09:18:12 +0200 Subject: [PATCH] Do not report `static` in PHPDoc tags above traits as an error --- conf/config.level0.neon | 5 + conf/config.level2.neon | 10 + src/Analyser/NodeScopeResolver.php | 5 +- src/Node/InTraitNode.php | 7 +- src/PhpDoc/StubValidator.php | 6 + src/Reflection/ClassReflection.php | 27 +++ src/Rules/Classes/LocalTypeAliasesCheck.php | 182 ++++++++++++------ .../Classes/LocalTypeTraitAliasesRule.php | 2 +- .../Classes/LocalTypeTraitUseAliasesRule.php | 34 ++++ src/Rules/Classes/MethodTagCheck.php | 150 ++++++++++++--- src/Rules/Classes/MethodTagRule.php | 5 +- src/Rules/Classes/MethodTagTraitRule.php | 2 +- src/Rules/Classes/MethodTagTraitUseRule.php | 34 ++++ src/Rules/Classes/PropertyTagCheck.php | 159 ++++++++++----- src/Rules/Classes/PropertyTagTraitRule.php | 2 +- src/Rules/Classes/PropertyTagTraitUseRule.php | 34 ++++ .../Analyser/NodeScopeResolverTest.php | 3 + .../Classes/LocalTypeTraitAliasesRuleTest.php | 5 + .../LocalTypeTraitUseAliasesRuleTest.php | 78 ++++++++ .../Rules/Classes/MethodTagTraitRuleTest.php | 21 +- .../Classes/MethodTagTraitUseRuleTest.php | 64 ++++++ .../Classes/PropertyTagTraitRuleTest.php | 11 +- .../Classes/PropertyTagTraitUseRuleTest.php | 55 ++++++ .../Classes/data/bug-11591-method-tag.php | 32 +++ .../Classes/data/bug-11591-property-tag.php | 26 +++ .../PHPStan/Rules/Classes/data/bug-11591.php | 44 +++++ .../Classes/data/local-type-trait-aliases.php | 13 ++ .../data/local-type-trait-use-aliases.php | 26 +++ .../Rules/Classes/data/method-tag-trait.php | 7 + .../Rules/Classes/data/property-tag-trait.php | 8 + 30 files changed, 898 insertions(+), 159 deletions(-) create mode 100644 src/Rules/Classes/LocalTypeTraitUseAliasesRule.php create mode 100644 src/Rules/Classes/MethodTagTraitUseRule.php create mode 100644 src/Rules/Classes/PropertyTagTraitUseRule.php create mode 100644 tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php create mode 100644 tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php create mode 100644 tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-11591-method-tag.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-11591.php create mode 100644 tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index d23705fa92..1382d99ee1 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -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 @@ -148,6 +150,9 @@ services: arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + - + class: PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule + - class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule tags: diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 72d297bfb3..c60247afb1 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -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: @@ -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: diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7c2118d058..a885d648b0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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; } diff --git a/src/Node/InTraitNode.php b/src/Node/InTraitNode.php index 39b0dc509b..2a3a810fb5 100644 --- a/src/Node/InTraitNode.php +++ b/src/Node/InTraitNode.php @@ -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()); } @@ -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'; diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index a1409ca917..1949fce656 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -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; @@ -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); diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index cc687fdfcc..be4ef76e9e 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -129,6 +129,8 @@ class ClassReflection private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false; + private false|ResolvedPhpDocBlock $traitContextResolvedPhpDocBlock = false; + /** @var ClassReflection[]|null */ private ?array $cachedInterfaces = null; @@ -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) { diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php index 71e807ecd7..5347681a90 100644 --- a/src/Rules/Classes/LocalTypeAliasesCheck.php +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -53,6 +53,22 @@ public function __construct( * @return list */ 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 + */ + public function checkInTraitDefinitionContext(ClassReflection $reflection): array { $phpDoc = $reflection->getResolvedPhpDoc(); if ($phpDoc === null) { @@ -69,7 +85,7 @@ public function check(ClassReflection $reflection, ClassLike $node): array }; $errors = []; - $className = $reflection->getName(); + $className = $reflection->getDisplayName(); $importedAliases = []; @@ -162,32 +178,7 @@ 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; } @@ -195,45 +186,78 @@ public function check(ClassReflection $reflection, ClassLike $node): array 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 + */ + 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)) @@ -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 $errors + * @param-out list $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; + } + } diff --git a/src/Rules/Classes/LocalTypeTraitAliasesRule.php b/src/Rules/Classes/LocalTypeTraitAliasesRule.php index 58d72696ad..1f7fe5021d 100644 --- a/src/Rules/Classes/LocalTypeTraitAliasesRule.php +++ b/src/Rules/Classes/LocalTypeTraitAliasesRule.php @@ -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())); } } diff --git a/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php b/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php new file mode 100644 index 0000000000..2523e3a9b4 --- /dev/null +++ b/src/Rules/Classes/LocalTypeTraitUseAliasesRule.php @@ -0,0 +1,34 @@ + + */ +final class LocalTypeTraitUseAliasesRule implements Rule +{ + + public function __construct(private LocalTypeAliasesCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/MethodTagCheck.php b/src/Rules/Classes/MethodTagCheck.php index 1c61442c9d..e8c44d416e 100644 --- a/src/Rules/Classes/MethodTagCheck.php +++ b/src/Rules/Classes/MethodTagCheck.php @@ -47,7 +47,10 @@ public function check( 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) { + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) { $errors[] = $error; } @@ -55,12 +58,20 @@ public function check( continue; } - foreach ($this->checkMethodType($classReflection, $methodName, sprintf('%s default value', $parameterDescription), $parameterTag->getDefaultValue(), $node) as $error) { + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue(), $node) as $error) { $errors[] = $error; } } - foreach ($this->checkMethodType($classReflection, $methodName, 'return type', $methodTag->getReturnType(), $node) as $error) { + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType()) as $error) { + $errors[] = $error; + } + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType(), $node) as $error) { $errors[] = $error; } } @@ -71,34 +82,86 @@ public function check( /** * @return list */ - private function checkMethodType(ClassReflection $classReflection, string $methodName, string $description, Type $type, ClassLike $node): array + public function checkInTraitDefinitionContext(ClassReflection $classReflection): 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(), - ]; + $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->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType()) as $error) { + $errors[] = $error; + } + + if ($parameterTag->getDefaultValue() === null) { + continue; + } + + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue()) as $error) { + $errors[] = $error; + } + } + + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitDefinitionContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType()) as $error) { + $errors[] = $error; + } } - $escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); - $escapedMethodName = SprintfHelper::escapeFormatString($methodName); - $escapedDescription = SprintfHelper::escapeFormatString($description); + return $errors; + } - $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), - ); + /** + * @return list + */ + public function checkInTraitUseContext( + ClassReflection $classReflection, + ClassReflection $implementingClass, + ClassLike $node, + ): array + { + $phpDoc = $classReflection->getTraitContextResolvedPhpDoc($implementingClass); + if ($phpDoc === null) { + return []; + } + $errors = []; + foreach ($phpDoc->getMethodTags() as $methodName => $methodTag) { + $i = 0; + foreach ($methodTag->getParameters() as $parameterName => $parameterTag) { + $i++; + $parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName); + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) { + $errors[] = $error; + } + + if ($parameterTag->getDefaultValue() === null) { + continue; + } + + $defaultValueDescription = sprintf('%s default value', $parameterDescription); + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $defaultValueDescription, $parameterTag->getDefaultValue(), $node) as $error) { + $errors[] = $error; + } + } + + $returnTypeDescription = 'return type'; + foreach ($this->checkMethodTypeInTraitUseContext($classReflection, $methodName, $returnTypeDescription, $methodTag->getReturnType(), $node) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classReflection, string $methodName, string $description, Type $type): array + { + $errors = []; 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', @@ -138,6 +201,15 @@ private function checkMethodType(ClassReflection $classReflection, string $metho ))->identifier('missingType.callable')->build(); } + return $errors; + } + + /** + * @return list + */ + private function checkMethodTypeInTraitUseContext(ClassReflection $classReflection, string $methodName, string $description, Type $type, ClassLike $node): array + { + $errors = []; 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)) @@ -158,7 +230,31 @@ private function checkMethodType(ClassReflection $classReflection, string $metho } } - return $errors; + if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) { + $errors[] = 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); + + return array_merge( + $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), + ), + ); } } diff --git a/src/Rules/Classes/MethodTagRule.php b/src/Rules/Classes/MethodTagRule.php index cdfc6759e7..ddb3cf254d 100644 --- a/src/Rules/Classes/MethodTagRule.php +++ b/src/Rules/Classes/MethodTagRule.php @@ -24,7 +24,10 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - return $this->check->check($node->getClassReflection(), $node->getOriginalNode()); + return $this->check->check( + $node->getClassReflection(), + $node->getOriginalNode(), + ); } } diff --git a/src/Rules/Classes/MethodTagTraitRule.php b/src/Rules/Classes/MethodTagTraitRule.php index 57f84a3941..157c46f7f4 100644 --- a/src/Rules/Classes/MethodTagTraitRule.php +++ b/src/Rules/Classes/MethodTagTraitRule.php @@ -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())); } } diff --git a/src/Rules/Classes/MethodTagTraitUseRule.php b/src/Rules/Classes/MethodTagTraitUseRule.php new file mode 100644 index 0000000000..1f6d6f1f7c --- /dev/null +++ b/src/Rules/Classes/MethodTagTraitUseRule.php @@ -0,0 +1,34 @@ + + */ +final class MethodTagTraitUseRule implements Rule +{ + + public function __construct(private MethodTagCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/src/Rules/Classes/PropertyTagCheck.php b/src/Rules/Classes/PropertyTagCheck.php index abbc274698..788c252d47 100644 --- a/src/Rules/Classes/PropertyTagCheck.php +++ b/src/Rules/Classes/PropertyTagCheck.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt\ClassLike; use PHPStan\Internal\SprintfHelper; +use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassNameCheck; @@ -44,32 +45,30 @@ public function check( { $errors = []; foreach ($classReflection->getPropertyTags() as $propertyName => $propertyTag) { - $readableType = $propertyTag->getReadableType(); - $writableType = $propertyTag->getWritableType(); - - $types = []; - $tagName = '@property'; - if ($readableType !== null) { - if ($writableType !== null) { - if ($writableType->equals($readableType)) { - $types[] = $readableType; - } else { - $types[] = $readableType; - $types[] = $writableType; - } - } else { - $tagName = '@property-read'; - $types[] = $readableType; + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); + foreach ($types as $type) { + foreach ($this->checkPropertyTypeInTraitDefinitionContext($classReflection, $propertyName, $tagName, $type) as $error) { + $errors[] = $error; + } + foreach ($this->checkPropertyTypeInTraitUseContext($classReflection, $propertyName, $tagName, $type, $node) as $error) { + $errors[] = $error; } - } elseif ($writableType !== null) { - $tagName = '@property-write'; - $types[] = $writableType; - } else { - throw new ShouldNotHappenException(); } + } + return $errors; + } + + /** + * @return list + */ + public function checkInTraitDefinitionContext(ClassReflection $classReflection): array + { + $errors = []; + foreach ($classReflection->getPropertyTags() as $propertyName => $propertyTag) { + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); foreach ($types as $type) { - foreach ($this->checkPropertyType($classReflection, $propertyName, $tagName, $type, $node) as $error) { + foreach ($this->checkPropertyTypeInTraitDefinitionContext($classReflection, $propertyName, $tagName, $type) as $error) { $errors[] = $error; } } @@ -81,33 +80,68 @@ public function check( /** * @return list */ - private function checkPropertyType(ClassReflection $classReflection, string $propertyName, string $tagName, Type $type, ClassLike $node): array + public function checkInTraitUseContext( + ClassReflection $classReflection, + ClassReflection $implementingClass, + ClassLike $node, + ): array { - if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) { - return [ - RuleErrorBuilder::message(sprintf( - 'PHPDoc tag %s for property %s::$%s contains unresolvable type.', - $tagName, - $classReflection->getDisplayName(), - $propertyName, - ))->identifier('propertyTag.unresolvableType') - ->build(), - ]; + $phpDoc = $classReflection->getTraitContextResolvedPhpDoc($implementingClass); + if ($phpDoc === null) { + return []; } - $escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); - $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); - $escapedTagName = SprintfHelper::escapeFormatString($tagName); + $errors = []; + foreach ($phpDoc->getPropertyTags() as $propertyName => $propertyTag) { + [$types, $tagName] = $this->getTypesAndTagName($propertyTag); + foreach ($types as $type) { + foreach ($this->checkPropertyTypeInTraitUseContext($classReflection, $propertyName, $tagName, $type, $node) as $error) { + $errors[] = $error; + } + } + } - $errors = $this->genericObjectTypeCheck->check( - $type, - sprintf('PHPDoc tag %s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $escapedTagName, $escapedClassName, $escapedPropertyName), - sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), - sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), - sprintf('Type %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), - sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), - sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTagName, $escapedClassName, $escapedPropertyName), - ); + return $errors; + } + + /** + * @return array{list, string} + */ + private function getTypesAndTagName(PropertyTag $propertyTag): array + { + $readableType = $propertyTag->getReadableType(); + $writableType = $propertyTag->getWritableType(); + + $types = []; + $tagName = '@property'; + if ($readableType !== null) { + if ($writableType !== null) { + if ($writableType->equals($readableType)) { + $types[] = $readableType; + } else { + $types[] = $readableType; + $types[] = $writableType; + } + } else { + $tagName = '@property-read'; + $types[] = $readableType; + } + } elseif ($writableType !== null) { + $tagName = '@property-write'; + $types[] = $writableType; + } else { + throw new ShouldNotHappenException(); + } + + return [$types, $tagName]; + } + + /** + * @return list + */ + private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $classReflection, string $propertyName, string $tagName, Type $type): array + { + $errors = []; foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { $errors[] = RuleErrorBuilder::message(sprintf( @@ -148,6 +182,15 @@ private function checkPropertyType(ClassReflection $classReflection, string $pro ))->identifier('missingType.callable')->build(); } + return $errors; + } + + /** + * @return list + */ + private function checkPropertyTypeInTraitUseContext(ClassReflection $classReflection, string $propertyName, string $tagName, Type $type, ClassLike $node): array + { + $errors = []; foreach ($type->getReferencedClasses() as $class) { if (!$this->reflectionProvider->hasClass($class)) { $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag %s for property %s::$%s contains unknown class %s.', $tagName, $classReflection->getDisplayName(), $propertyName, $class)) @@ -168,7 +211,31 @@ private function checkPropertyType(ClassReflection $classReflection, string $pro } } - return $errors; + if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag %s for property %s::$%s contains unresolvable type.', + $tagName, + $classReflection->getDisplayName(), + $propertyName, + ))->identifier('propertyTag.unresolvableType')->build(); + } + + $escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + $escapedTagName = SprintfHelper::escapeFormatString($tagName); + + return array_merge( + $errors, + $this->genericObjectTypeCheck->check( + $type, + sprintf('PHPDoc tag %s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Generic type %%s in PHPDoc tag %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Type %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is in conflict with %%s template type %%s of %%s %%s.', $escapedTagName, $escapedClassName, $escapedPropertyName), + sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag %s for property %s::$%s is redundant, template type %%s of %%s %%s has the same variance.', $escapedTagName, $escapedClassName, $escapedPropertyName), + ), + ); } } diff --git a/src/Rules/Classes/PropertyTagTraitRule.php b/src/Rules/Classes/PropertyTagTraitRule.php index cd3a54c9fd..bd5de407ba 100644 --- a/src/Rules/Classes/PropertyTagTraitRule.php +++ b/src/Rules/Classes/PropertyTagTraitRule.php @@ -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())); } } diff --git a/src/Rules/Classes/PropertyTagTraitUseRule.php b/src/Rules/Classes/PropertyTagTraitUseRule.php new file mode 100644 index 0000000000..f381cd0dfd --- /dev/null +++ b/src/Rules/Classes/PropertyTagTraitUseRule.php @@ -0,0 +1,34 @@ + + */ +final class PropertyTagTraitUseRule implements Rule +{ + + public function __construct(private PropertyTagCheck $check) + { + } + + public function getNodeType(): string + { + return InTraitNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->check->checkInTraitUseContext( + $node->getTraitReflection(), + $node->getImplementingClassReflection(), + $node->getOriginalNode(), + ); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6a8c6b517d..a5aac09b6e 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -181,6 +181,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Functions/data/bug-9803.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/PhpDoc/data/bug-10594.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-11591.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-11591-method-tag.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'); } /** diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php index 1501b40f79..fb443854df 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitAliasesRuleTest.php @@ -117,4 +117,9 @@ public function testRule(): void ]); } + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php new file mode 100644 index 0000000000..58ddda39a4 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/LocalTypeTraitUseAliasesRuleTest.php @@ -0,0 +1,78 @@ + + */ +class LocalTypeTraitUseAliasesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new LocalTypeTraitUseAliasesRule( + new LocalTypeAliasesCheck( + ['GlobalTypeAlias' => 'int|string'], + $this->createReflectionProvider(), + self::getContainer()->getByType(TypeNodeResolver::class), + new MissingTypehintCheck(true, true, true, true, []), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new UnresolvableTypeHelper(), + new GenericObjectTypeCheck(), + true, + true, + true, + ), + ); + } + + public function testRule(): void + { + // everything reported by LocalTypeTraitAliasesRule + $this->analyse([__DIR__ . '/data/local-type-trait-aliases.php'], []); + } + + public function testRuleSpecific(): void + { + $this->analyse([__DIR__ . '/data/local-type-trait-use-aliases.php'], [ + [ + 'Type alias A contains unknown class LocalTypeTraitUseAliases\Nonexistent.', + 16, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Type alias B contains invalid type LocalTypeTraitUseAliases\SomeTrait.', + 16, + ], + [ + 'Type alias C contains unresolvable type.', + 16, + ], + [ + 'Type alias D contains generic type Exception but class Exception is not generic.', + 16, + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php b/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php index 543e0d9d70..a2c07386e2 100644 --- a/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitRuleTest.php @@ -39,27 +39,18 @@ protected function getRule(): TRule public function testRule(): void { - $fooTraitLine = 12; $this->analyse([__DIR__ . '/data/method-tag-trait.php'], [ - [ - 'PHPDoc tag @method for method MethodTagTrait\Foo::doFoo() return type contains unknown class MethodTagTrait\intt.', - $fooTraitLine, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', - ], - [ - 'PHPDoc tag @method for method MethodTagTrait\Foo::doBar() parameter #1 $a contains unresolvable type.', - $fooTraitLine, - ], - [ - 'PHPDoc tag @method for method MethodTagTrait\Foo::doBaz2() parameter #1 $a default value contains unresolvable type.', - $fooTraitLine, - ], [ 'Trait MethodTagTrait\Foo has PHPDoc tag @method for method doMissingIterablueValue() return type with no value type specified in iterable type array.', - $fooTraitLine, + 12, MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); } + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-method-tag.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php new file mode 100644 index 0000000000..4e775a4857 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/MethodTagTraitUseRuleTest.php @@ -0,0 +1,64 @@ + + */ +class MethodTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new MethodTagTraitUseRule( + new MethodTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, true, true, true, []), + new UnresolvableTypeHelper(), + true, + ), + ); + } + + public function testRule(): void + { + $fooTraitLine = 12; + $this->analyse([__DIR__ . '/data/method-tag-trait.php'], [ + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doFoo() return type contains unknown class MethodTagTrait\intt.', + $fooTraitLine, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doBar() parameter #1 $a contains unresolvable type.', + $fooTraitLine, + ], + [ + 'PHPDoc tag @method for method MethodTagTrait\Foo::doBaz2() parameter #1 $a default value contains unresolvable type.', + $fooTraitLine, + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-method-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php b/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php index c6e140604c..887cebd583 100644 --- a/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitRuleTest.php @@ -41,11 +41,16 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/property-tag-trait.php'], [ [ - 'PHPDoc tag @property for property PropertyTagTrait\Foo::$foo contains unknown class PropertyTagTrait\intt.', - 8, - 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + 'Trait PropertyTagTrait\Foo has PHPDoc tag @property for property $bar with no value type specified in iterable type array.', + 9, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, ], ]); } + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-property-tag.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php new file mode 100644 index 0000000000..c19a36419a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/PropertyTagTraitUseRuleTest.php @@ -0,0 +1,55 @@ + + */ +class PropertyTagTraitUseRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new PropertyTagTraitUseRule( + new PropertyTagCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, true, true, true, []), + new UnresolvableTypeHelper(), + true, + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/property-tag-trait.php'], [ + [ + 'PHPDoc tag @property for property PropertyTagTrait\Foo::$foo contains unknown class PropertyTagTrait\intt.', + 9, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug11591(): void + { + $this->analyse([__DIR__ . '/data/bug-11591-property-tag.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591-method-tag.php b/tests/PHPStan/Rules/Classes/data/bug-11591-method-tag.php new file mode 100644 index 0000000000..7ef678f8fb --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591-method-tag.php @@ -0,0 +1,32 @@ + withTrashed(bool $withTrashed = true) + * @method static Builder onlyTrashed() + * @method static Builder withoutTrashed() + * @method static bool restore() + * @method static static restoreOrCreate(array $attributes = [], array $values = []) + * @method static static createOrRestore(array $attributes = [], array $values = []) + */ +trait SoftDeletes {} + +function test(): void { + assertType('Bug11591MethodTag\\Builder', User::withTrashed()); + assertType('Bug11591MethodTag\\Builder', User::onlyTrashed()); + assertType('Bug11591MethodTag\\Builder', User::withoutTrashed()); + assertType(User::class, User::createOrRestore()); + assertType(User::class, User::restoreOrCreate()); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php new file mode 100644 index 0000000000..c9bf36e246 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591-property-tag.php @@ -0,0 +1,26 @@ + $a + * @property static $b + */ +trait SoftDeletes {} + +function test(User $user): void { + assertType('Bug11591PropertyTag\\Builder', $user->a); + assertType(User::class, $user->b); +} diff --git a/tests/PHPStan/Rules/Classes/data/bug-11591.php b/tests/PHPStan/Rules/Classes/data/bug-11591.php new file mode 100644 index 0000000000..e41413653a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-11591.php @@ -0,0 +1,44 @@ + + */ +trait WithConfig { + /** + * @param SettingsFactory $settings + */ + public function setConfig(callable $settings): void { + $settings($this); + } + + /** + * @param callable(static): array $settings + */ + public function setConfig2(callable $settings): void { + $settings($this); + } + + /** + * @param callable(self): array $settings + */ + public function setConfig3(callable $settings): void { + $settings($this); + } +} + +class A +{ + use WithConfig; +} + +function (A $a): void { + $a->setConfig(function ($who) { + assertType(A::class, $who); + + return []; + }); +}; diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php index 6aaa554d52..6628e0db7c 100644 --- a/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-aliases.php @@ -70,3 +70,16 @@ trait MissingType { } + +class Usages +{ + + use Foo; + use Bar; + use Baz; + use Qux; + use Generic; + use Invalid; + use MissingType; + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php new file mode 100644 index 0000000000..94e019280c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-trait-use-aliases.php @@ -0,0 +1,26 @@ + + */ +trait Foo +{ + +} + +class Usage +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/method-tag-trait.php b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php index 149d43a854..504033696a 100644 --- a/tests/PHPStan/Rules/Classes/data/method-tag-trait.php +++ b/tests/PHPStan/Rules/Classes/data/method-tag-trait.php @@ -21,3 +21,10 @@ class ClassWithConstant public const FOO = 1; } + +class Usages +{ + + use Foo; + +} diff --git a/tests/PHPStan/Rules/Classes/data/property-tag-trait.php b/tests/PHPStan/Rules/Classes/data/property-tag-trait.php index c5a50f30dd..9bee89824e 100644 --- a/tests/PHPStan/Rules/Classes/data/property-tag-trait.php +++ b/tests/PHPStan/Rules/Classes/data/property-tag-trait.php @@ -4,8 +4,16 @@ /** * @property intt $foo + * @property array $bar */ trait Foo { } + +class Usages +{ + + use Foo; + +}