diff --git a/conf/config.level2.neon b/conf/config.level2.neon index e43d074210..efbc7b7653 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -83,9 +83,6 @@ conditionalTags: services: - class: PHPStan\Rules\Classes\MixinRule - arguments: - checkClassCaseSensitivity: %checkClassCaseSensitivity% - absentTypeChecks: %featureToggles.absentTypeChecks% tags: - phpstan.rules.rule diff --git a/conf/config.neon b/conf/config.neon index 704b061171..9f62638b65 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -923,6 +923,12 @@ services: arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% + - + class: PHPStan\Rules\Classes\MixinCheck + arguments: + checkClassCaseSensitivity: %checkClassCaseSensitivity% + absentTypeChecks: %featureToggles.absentTypeChecks% + - class: PHPStan\Rules\Classes\PropertyTagCheck arguments: diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index fc6a44ee28..e639533f64 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -31,6 +31,7 @@ use PHPStan\Rules\Classes\MethodTagRule; use PHPStan\Rules\Classes\MethodTagTraitRule; use PHPStan\Rules\Classes\MethodTagTraitUseRule; +use PHPStan\Rules\Classes\MixinCheck; use PHPStan\Rules\Classes\MixinRule; use PHPStan\Rules\Classes\PropertyTagCheck; use PHPStan\Rules\Classes\PropertyTagRule; @@ -182,6 +183,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); $genericCallableRuleHelper = $container->getByType(GenericCallableRuleHelper::class); $methodTagTemplateTypeCheck = $container->getByType(MethodTagTemplateTypeCheck::class); + $mixinCheck = $container->getByType(MixinCheck::class); $rules = [ // level 0 @@ -256,7 +258,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $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 MixinRule($mixinCheck); $rules[] = new LocalTypeTraitUseAliasesRule($localTypeAliasesCheck); $rules[] = new MethodTagTemplateTypeTraitRule($methodTagTemplateTypeCheck, $reflectionProvider); } diff --git a/src/Rules/Classes/MixinCheck.php b/src/Rules/Classes/MixinCheck.php new file mode 100644 index 0000000000..e732f6d9e6 --- /dev/null +++ b/src/Rules/Classes/MixinCheck.php @@ -0,0 +1,128 @@ + + */ + public function check(ClassReflection $classReflection, ClassLike $node): array + { + $mixinTags = $classReflection->getMixinTags(); + $errors = []; + foreach ($mixinTags as $mixinTag) { + $type = $mixinTag->getType(); + if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) + ->identifier('mixin.nonObject') + ->build(); + continue; + } + + if ( + $this->unresolvableTypeHelper->containsUnresolvableType($type) + ) { + $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.') + ->identifier('mixin.unresolvableType') + ->build(); + continue; + } + + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( + $type, + 'PHPDoc tag @mixin contains generic type %s but %s %s is not generic.', + 'Generic type %s in PHPDoc tag @mixin does not specify all template types of %s %s: %s', + 'Generic type %s in PHPDoc tag @mixin specifies %d template types, but %s %s supports only %d: %s', + 'Type %s in generic type %s in PHPDoc tag @mixin is not subtype of template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is in conflict with %s template type %s of %s %s.', + 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is redundant, template type %s of %s %s has the same variance.', + )); + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', + $innerName, + implode(', ', $genericTypeNames), + )) + ->identifier('missingType.generics') + ->build(); + } + + if ($this->absentTypeChecks) { + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + '%s %s has PHPDoc tag @mixin with no value type specified in iterable type %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $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 @mixin with no signature specified for %s.', + $classReflection->getClassTypeDescription(), + $classReflection->getDisplayName(), + $callableType->describe(VerbosityLevel::typeOnly()), + ))->identifier('missingType.callable')->build(); + } + } + + foreach ($type->getReferencedClasses() as $class) { + if (!$this->reflectionProvider->hasClass($class)) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class)) + ->identifier('class.notFound') + ->discoveringSymbolsTip() + ->build(); + } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class)) + ->identifier('mixin.trait') + ->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $node), + ], $this->checkClassCaseSensitivity), + ); + } + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/MixinRule.php b/src/Rules/Classes/MixinRule.php index 56f19a7203..8fb28e0888 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -5,18 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassNameCheck; -use PHPStan\Rules\ClassNameNodePair; -use PHPStan\Rules\Generics\GenericObjectTypeCheck; -use PHPStan\Rules\MissingTypehintCheck; -use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\VerbosityLevel; -use function array_merge; -use function implode; -use function sprintf; /** * @implements Rule @@ -24,15 +13,7 @@ final class MixinRule implements Rule { - public function __construct( - private ReflectionProvider $reflectionProvider, - private ClassNameCheck $classCheck, - private GenericObjectTypeCheck $genericObjectTypeCheck, - private MissingTypehintCheck $missingTypehintCheck, - private UnresolvableTypeHelper $unresolvableTypeHelper, - private bool $checkClassCaseSensitivity, - private bool $absentTypeChecks, - ) + public function __construct(private MixinCheck $check) { } @@ -43,93 +24,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $node->getClassReflection(); - $mixinTags = $classReflection->getMixinTags(); - $errors = []; - foreach ($mixinTags as $mixinTag) { - $type = $mixinTag->getType(); - if (!$type->canCallMethods()->yes() || !$type->canAccessProperties()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly()))) - ->identifier('mixin.nonObject') - ->build(); - continue; - } - - if ( - $this->unresolvableTypeHelper->containsUnresolvableType($type) - ) { - $errors[] = RuleErrorBuilder::message('PHPDoc tag @mixin contains unresolvable type.') - ->identifier('mixin.unresolvableType') - ->build(); - continue; - } - - $errors = array_merge($errors, $this->genericObjectTypeCheck->check( - $type, - 'PHPDoc tag @mixin contains generic type %s but %s %s is not generic.', - 'Generic type %s in PHPDoc tag @mixin does not specify all template types of %s %s: %s', - 'Generic type %s in PHPDoc tag @mixin specifies %d template types, but %s %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @mixin is not subtype of template type %s of %s %s.', - 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is in conflict with %s template type %s of %s %s.', - 'Call-site variance of %s in generic type %s in PHPDoc tag @mixin is redundant, template type %s of %s %s has the same variance.', - )); - - foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', - $innerName, - implode(', ', $genericTypeNames), - )) - ->identifier('missingType.generics') - ->build(); - } - - if ($this->absentTypeChecks) { - foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) { - $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); - $errors[] = RuleErrorBuilder::message(sprintf( - '%s %s has PHPDoc tag @mixin with no value type specified in iterable type %s.', - $classReflection->getClassTypeDescription(), - $classReflection->getDisplayName(), - $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 @mixin with no signature specified for %s.', - $classReflection->getClassTypeDescription(), - $classReflection->getDisplayName(), - $callableType->describe(VerbosityLevel::typeOnly()), - ))->identifier('missingType.callable')->build(); - } - } - - foreach ($type->getReferencedClasses() as $class) { - if (!$this->reflectionProvider->hasClass($class)) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class)) - ->identifier('class.notFound') - ->discoveringSymbolsTip() - ->build(); - } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { - $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class)) - ->identifier('mixin.trait') - ->build(); - } else { - $errors = array_merge( - $errors, - $this->classCheck->checkClassNames([ - new ClassNameNodePair($class, $node), - ], $this->checkClassCaseSensitivity), - ); - } - } - } - - return $errors; + return $this->check->check($node->getClassReflection(), $node->getOriginalNode()); } } diff --git a/tests/PHPStan/Rules/Classes/MixinRuleTest.php b/tests/PHPStan/Rules/Classes/MixinRuleTest.php index 1d93a8a299..acaf1974b0 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -23,16 +23,18 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new MixinRule( - $reflectionProvider, - new ClassNameCheck( - new ClassCaseSensitivityCheck($reflectionProvider, true), - new ClassForbiddenNameCheck(self::getContainer()), + new MixinCheck( + $reflectionProvider, + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), + new GenericObjectTypeCheck(), + new MissingTypehintCheck(true, true, true, true, []), + new UnresolvableTypeHelper(), + true, + true, ), - new GenericObjectTypeCheck(), - new MissingTypehintCheck(true, true, true, true, []), - new UnresolvableTypeHelper(), - true, - true, ); }