diff --git a/packages/NodeTypeResolver/NodeTypeResolver.php b/packages/NodeTypeResolver/NodeTypeResolver.php index 4df26d45e7e2..629fc4bb661c 100644 --- a/packages/NodeTypeResolver/NodeTypeResolver.php +++ b/packages/NodeTypeResolver/NodeTypeResolver.php @@ -352,6 +352,9 @@ public function isPropertyBoolean(Property $property): bool return $this->isStaticType($defaultNodeValue, BooleanType::class); } + /** + * @return class-string + */ public function getFullyQualifiedClassName(TypeWithClassName $typeWithClassName): string { if ($typeWithClassName instanceof ShortenedObjectType) { diff --git a/packages/PHPStanStaticTypeMapper/Contract/TypeMapperInterface.php b/packages/PHPStanStaticTypeMapper/Contract/TypeMapperInterface.php index 695eb851f846..6935cf574048 100644 --- a/packages/PHPStanStaticTypeMapper/Contract/TypeMapperInterface.php +++ b/packages/PHPStanStaticTypeMapper/Contract/TypeMapperInterface.php @@ -13,6 +13,9 @@ interface TypeMapperInterface { + /** + * @return class-string + */ public function getNodeClass(): string; public function mapToPHPStanPhpDocTypeNode(Type $type): TypeNode; diff --git a/packages/StaticTypeMapper/Contract/PhpDocParser/PhpDocTypeMapperInterface.php b/packages/StaticTypeMapper/Contract/PhpDocParser/PhpDocTypeMapperInterface.php index fdcdc3b5625e..905174fd5d7b 100644 --- a/packages/StaticTypeMapper/Contract/PhpDocParser/PhpDocTypeMapperInterface.php +++ b/packages/StaticTypeMapper/Contract/PhpDocParser/PhpDocTypeMapperInterface.php @@ -11,6 +11,9 @@ interface PhpDocTypeMapperInterface { + /** + * @return class-string + */ public function getNodeType(): string; public function mapToPHPStanType(TypeNode $typeNode, Node $node, NameScope $nameScope): Type; diff --git a/packages/StaticTypeMapper/PhpDocParser/IdentifierTypeMapper.php b/packages/StaticTypeMapper/PhpDocParser/IdentifierTypeMapper.php index ab59951b06ee..8db3b8c6de8d 100644 --- a/packages/StaticTypeMapper/PhpDocParser/IdentifierTypeMapper.php +++ b/packages/StaticTypeMapper/PhpDocParser/IdentifierTypeMapper.php @@ -49,6 +49,9 @@ public function __construct( $this->parentClassScopeResolver = $parentClassScopeResolver; } + /** + * @return class-string + */ public function getNodeType(): string { return IdentifierTypeNode::class; diff --git a/packages/StaticTypeMapper/PhpDocParser/UnionTypeMapper.php b/packages/StaticTypeMapper/PhpDocParser/UnionTypeMapper.php index ba9805bd2051..bfbdd1db4169 100644 --- a/packages/StaticTypeMapper/PhpDocParser/UnionTypeMapper.php +++ b/packages/StaticTypeMapper/PhpDocParser/UnionTypeMapper.php @@ -30,6 +30,9 @@ public function __construct(TypeFactory $typeFactory) $this->typeFactory = $typeFactory; } + /** + * @return class-string + */ public function getNodeType(): string { return UnionTypeNode::class; diff --git a/phpstan.neon b/phpstan.neon index fc68da2cb734..f25fc324f0b5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -519,3 +519,10 @@ parameters: # known types - '#Parameter \#1 \$node of method Rector\\Naming\\Naming\\VariableNaming\:\:resolveFromMethodCall\(\) expects PhpParser\\Node\\Expr\\MethodCall\|PhpParser\\Node\\Expr\\NullsafeMethodCall\|PhpParser\\Node\\Expr\\StaticCall, PhpParser\\Node given#' + + - + message: '#Do not inherit from abstract class, better use composition#' + path: utils/phpstan-extensions/src/Rule/NoInstanceOfStaticReflectionRule.php + + # first wave resolved, merge PR + - '#Instead of "(.*?)" use ReflectionProvider service (.*?) for static reflection to work#' diff --git a/rules/Autodiscovery/Rector/FileNode/MoveValueObjectsToValueObjectDirectoryRector.php b/rules/Autodiscovery/Rector/FileNode/MoveValueObjectsToValueObjectDirectoryRector.php index d670a2f5214a..f69455262727 100644 --- a/rules/Autodiscovery/Rector/FileNode/MoveValueObjectsToValueObjectDirectoryRector.php +++ b/rules/Autodiscovery/Rector/FileNode/MoveValueObjectsToValueObjectDirectoryRector.php @@ -8,6 +8,7 @@ use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; +use PHPStan\Type\ObjectType; use Rector\Autodiscovery\Analyzer\ValueObjectClassAnalyzer; use Rector\Core\Contract\Rector\ConfigurableRectorInterface; use Rector\Core\PhpParser\Node\CustomNode\FileNode; @@ -55,7 +56,7 @@ final class MoveValueObjectsToValueObjectDirectoryRector extends AbstractRector private $enableValueObjectGuessing = true; /** - * @var string[] + * @var class-string[] */ private $types = []; @@ -169,6 +170,9 @@ public function refactor(Node $node): ?Node return null; } + /** + * @param array $configuration + */ public function configure(array $configuration): void { $this->types = $configuration[self::TYPES] ?? []; @@ -187,8 +191,11 @@ private function isValueObjectMatch(Class_ $class): bool return false; } + $classObjectType = new ObjectType($className); + foreach ($this->types as $type) { - if (is_a($className, $type, true)) { + $desiredObjectType = new ObjectType($type); + if ($desiredObjectType->isSuperTypeOf($classObjectType)->yes()) { return true; } } diff --git a/rules/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector.php b/rules/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector.php index c936bf2b3554..4c9677892bb5 100644 --- a/rules/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector.php +++ b/rules/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector.php @@ -265,10 +265,12 @@ private function createArgFromSpreadArrayItem(Scope $nodeScope, ArrayItem $array */ private function isIterableType(Type $type): bool { - return $type instanceof IterableType || ($type instanceof ObjectType && is_a( - $type->getClassName(), - Traversable::class, - true - )); + if ($type instanceof IterableType) { + return true; + } + + $traversableObjectType = new ObjectType('Traversable'); + return $traversableObjectType->isSuperTypeOf($type) + ->yes(); } } diff --git a/src/NodeManipulator/BinaryOpManipulator.php b/src/NodeManipulator/BinaryOpManipulator.php index 9009f07381d2..49ab80aed960 100644 --- a/src/NodeManipulator/BinaryOpManipulator.php +++ b/src/NodeManipulator/BinaryOpManipulator.php @@ -30,8 +30,8 @@ public function __construct(AssignAndBinaryMap $assignAndBinaryMap) * Tries to match left or right parts (xor), * returns null or match on first condition and then second condition. No matter what the origin order is. * - * @param callable|string $firstCondition callable or Node to instanceof - * @param callable|string $secondCondition callable or Node to instanceof + * @param callable|class-string $firstCondition + * @param callable|class-string $secondCondition */ public function matchFirstAndSecondConditionNode( BinaryOp $binaryOp, @@ -114,7 +114,7 @@ public function inverseNode(Expr $expr): Node } /** - * @param string|callable $firstCondition + * @param callable|class-string $firstCondition */ private function validateCondition($firstCondition): void { @@ -130,7 +130,7 @@ private function validateCondition($firstCondition): void } /** - * @param callable|string $condition + * @param callable|class-string $condition */ private function normalizeCondition($condition): callable { diff --git a/stubs/Kdyby/Events/Subscriber.php b/stubs/Kdyby/Events/Subscriber.php deleted file mode 100644 index 061061455192..000000000000 --- a/stubs/Kdyby/Events/Subscriber.php +++ /dev/null @@ -1,14 +0,0 @@ -findInstanceOf() diff --git a/utils/phpstan-extensions/config/rector-rules.neon b/utils/phpstan-extensions/config/rector-rules.neon index e77894401038..b55f372adaf0 100644 --- a/utils/phpstan-extensions/config/rector-rules.neon +++ b/utils/phpstan-extensions/config/rector-rules.neon @@ -1,4 +1,8 @@ services: + - + class: Rector\PHPStanExtensions\Rule\NoInstanceOfStaticReflectionRule + tags: [phpstan.rules.rule] + - class: Symplify\PHPStanRules\ObjectCalisthenics\Rules\NoChainMethodCallRule tags: [phpstan.rules.rule] @@ -13,9 +17,7 @@ services: allowedParentTypes: - Rector\Utils\DoctrineAnnotationParserSyncer\FileSyncer\AbstractClassSyncer - PhpParser\NodeAbstract - - PHPStan\Type\BooleanType - - PHPStan\Type\ObjectType - - PHPStan\Type\StaticType + - PHPStan\Type\Type - Rector\BetterPhpDocParser\PhpDocNodeFactory\AbstractPhpDocNodeFactory - PHPStan\PhpDocParser\Parser\PhpDocParser - Rector\PostRector\Rector\AbstractPostRector diff --git a/utils/phpstan-extensions/src/Rule/NoInstanceOfStaticReflectionRule.php b/utils/phpstan-extensions/src/Rule/NoInstanceOfStaticReflectionRule.php new file mode 100644 index 000000000000..bfa7e8ca93fa --- /dev/null +++ b/utils/phpstan-extensions/src/Rule/NoInstanceOfStaticReflectionRule.php @@ -0,0 +1,113 @@ +))->isSuperTypeOf()" for static reflection to work'; + + /** + * @var AllowedAutoloadedTypeAnalyzer + */ + private $allowedAutoloadedTypeAnalyzer; + + /** + * @var SimpleNameResolver + */ + private $simpleNameResolver; + + public function __construct( + SimpleNameResolver $simpleNameResolver, + AllowedAutoloadedTypeAnalyzer $allowedAutoloadedTypeAnalyzer + ) { + $this->allowedAutoloadedTypeAnalyzer = $allowedAutoloadedTypeAnalyzer; + $this->simpleNameResolver = $simpleNameResolver; + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Instanceof_::class, FuncCall::class]; + } + + /** + * @param Instanceof_|FuncCall $node + * @return string[] + */ + public function process(Node $node, Scope $scope): array + { + $exprStaticType = $this->resolveExprStaticType($node, $scope); + if ($exprStaticType === null) { + return []; + } + + if ($this->allowedAutoloadedTypeAnalyzer->isAllowedType($exprStaticType)) { + return []; + } + + return [self::ERROR_MESSAGE]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition(self::ERROR_MESSAGE, [ + new CodeSample( + <<<'CODE_SAMPLE' +return is_a($node, 'Command', true); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$nodeType = $scope->getType($node); +$commandObjectType = new ObjectType('Command'); + +return $commandObjectType->isSuperTypeOf($nodeType)->yes(); +CODE_SAMPLE + ), + ]); + } + + /** + * @param FuncCall|Instanceof_ $node + */ + private function resolveExprStaticType(Node $node, Scope $scope): ?Type + { + if ($node instanceof Instanceof_) { + if ($node->class instanceof Name) { + return new ConstantStringType($node->class->toString()); + } + + return $scope->getType($node->class); + } + + if (! $this->simpleNameResolver->isName($node, 'is_a')) { + return null; + } + + $typeArgValue = $node->args[1]->value; + return $scope->getType($typeArgValue); + } +} diff --git a/utils/phpstan-extensions/src/Rule/RequireRectorCategoryByGetNodeTypesRule.php b/utils/phpstan-extensions/src/Rule/RequireRectorCategoryByGetNodeTypesRule.php index fe8073b58d24..c3e4f99706cd 100644 --- a/utils/phpstan-extensions/src/Rule/RequireRectorCategoryByGetNodeTypesRule.php +++ b/utils/phpstan-extensions/src/Rule/RequireRectorCategoryByGetNodeTypesRule.php @@ -12,6 +12,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; +use Rector\Core\Contract\Rector\RectorInterface; use Rector\Core\Exception\ShouldNotHappenException; /** @@ -78,6 +79,10 @@ private function resolveRectorClassReflection(ClassMethod $classMethod, Scope $s return null; } + if (! $classReflection->isSubclassOf(RectorInterface::class)) { + return null; + } + if ($classReflection->isInterface()) { return null; } diff --git a/utils/phpstan-extensions/src/TypeAnalyzer/AllowedAutoloadedTypeAnalyzer.php b/utils/phpstan-extensions/src/TypeAnalyzer/AllowedAutoloadedTypeAnalyzer.php new file mode 100644 index 000000000000..fd471d16a158 --- /dev/null +++ b/utils/phpstan-extensions/src/TypeAnalyzer/AllowedAutoloadedTypeAnalyzer.php @@ -0,0 +1,70 @@ + + */ + private const ALLOWED_CLASSES = [DateTimeInterface::class, 'Symplify\SmartFileSystem\SmartFileInfo']; + + public function isAllowedType(Type $type): bool + { + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $unionedType) { + if (! $this->isAllowedType($unionedType)) { + return false; + } + } + + return true; + } + + if ($type instanceof ConstantStringType) { + return $this->isAllowedClassString($type->getValue()); + } + + if ($type instanceof ObjectType) { + return $this->isAllowedClassString($type->getClassName()); + } + + if ($type instanceof GenericClassStringType) { + return $this->isAllowedType($type->getGenericType()); + } + + return false; + } + + private function isAllowedClassString(string $value): bool + { + // autoloaded allowed type + if (Strings::match($value, self::AUTOLOADED_CLASS_PREFIX_REGEX)) { + return true; + } + + foreach (self::ALLOWED_CLASSES as $allowedClass) { + if ($value === $allowedClass) { + return true; + } + } + + return false; + } +} diff --git a/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/InstanceofWithType.php b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/InstanceofWithType.php new file mode 100644 index 000000000000..39b4212f2ce2 --- /dev/null +++ b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/InstanceofWithType.php @@ -0,0 +1,17 @@ +> + */ + private const BEFORE_TRAIT_TYPES = [TraitUse::class, Property::class, ClassMethod::class]; + + public function find($node) + { + foreach (self::BEFORE_TRAIT_TYPES as $beforeTraitType) { + if (is_a($node, $beforeTraitType, true)) { + return true; + } + } + + return false; + } +} diff --git a/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipDateTime.php b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipDateTime.php new file mode 100644 index 000000000000..58195591c081 --- /dev/null +++ b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipDateTime.php @@ -0,0 +1,15 @@ + $type + */ + public function findParentType(Node $parent, string $type) + { + do { + if (is_a($parent, $type, true)) { + return $parent; + } + } while ($parent = $parent->getAttribute(AttributeKey::PARENT_NODE)); + } +} diff --git a/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipIsAsClassString.php b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipIsAsClassString.php new file mode 100644 index 000000000000..030d47effd50 --- /dev/null +++ b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipIsAsClassString.php @@ -0,0 +1,20 @@ + $desiredType + */ + private function hasOnlyStmtOfType(If_ $if, string $desiredType): bool + { + $stmts = $if->stmts; + return is_a($stmts[0], $desiredType); + } +} diff --git a/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipReflection.php b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipReflection.php new file mode 100644 index 000000000000..f4e86bc2d62e --- /dev/null +++ b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/Fixture/SkipReflection.php @@ -0,0 +1,19 @@ + $expectedErrorsWithLines + */ + public function testRule(string $filePath, array $expectedErrorsWithLines): void + { + $this->analyse([$filePath], $expectedErrorsWithLines); + } + + public function provideData(): Iterator + { + $errorMessage = NoInstanceOfStaticReflectionRule::ERROR_MESSAGE; + yield [__DIR__ . '/Fixture/InstanceofWithType.php', [[$errorMessage, 13]]]; + + $errorMessage = NoInstanceOfStaticReflectionRule::ERROR_MESSAGE; + yield [__DIR__ . '/Fixture/IsAWithType.php', [[$errorMessage, 13]]]; + + yield [__DIR__ . '/Fixture/SkipAllowedType.php', []]; + yield [__DIR__ . '/Fixture/SkipGenericNodeType.php', []]; + yield [__DIR__ . '/Fixture/SkipIsAGenericClassString.php', []]; + yield [__DIR__ . '/Fixture/SkipIsAsClassString.php', []]; + yield [__DIR__ . '/Fixture/SkipFileInfo.php', []]; + yield [__DIR__ . '/Fixture/SkipArrayClassString.php', []]; + yield [__DIR__ . '/Fixture/SkipReflection.php', []]; + yield [__DIR__ . '/Fixture/SkipDateTime.php', []]; + } + + protected function getRule(): Rule + { + return $this->getRuleFromConfig( + NoInstanceOfStaticReflectionRule::class, + __DIR__ . '/config/configured_rule.neon' + ); + } +} diff --git a/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/config/configured_rule.neon b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/config/configured_rule.neon new file mode 100644 index 000000000000..06b7ed35a2dc --- /dev/null +++ b/utils/phpstan-extensions/tests/Rule/NoInstanceOfStaticReflectionRule/config/configured_rule.neon @@ -0,0 +1,9 @@ +includes: + - ../../../../../../vendor/symplify/phpstan-rules/config/services/services.neon + +services: + - Rector\PHPStanExtensions\TypeAnalyzer\AllowedAutoloadedTypeAnalyzer + + - + class: Rector\PHPStanExtensions\Rule\NoInstanceOfStaticReflectionRule + tags: [phpstan.rules.rule]