[] */ private const ALLOWED_NOT_READONLY_ANNOTATION_CLASS_OR_ATTRIBUTES = ['Doctrine\\ORM\\Mapping\\Entity', 'Doctrine\\ORM\\Mapping\\Table', 'Doctrine\\ORM\\Mapping\\MappedSuperclass']; /** * @readonly * @var \Rector\Core\NodeManipulator\AssignManipulator */ private $assignManipulator; /** * @readonly * @var \Rector\Core\PhpParser\Node\BetterNodeFinder */ private $betterNodeFinder; /** * @readonly * @var \Rector\ReadWrite\Guard\VariableToConstantGuard */ private $variableToConstantGuard; /** * @readonly * @var \Rector\ReadWrite\NodeAnalyzer\ReadWritePropertyAnalyzer */ private $readWritePropertyAnalyzer; /** * @readonly * @var \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory */ private $phpDocInfoFactory; /** * @readonly * @var \Rector\Core\PhpParser\NodeFinder\PropertyFetchFinder */ private $propertyFetchFinder; /** * @readonly * @var \Rector\Core\Reflection\ReflectionResolver */ private $reflectionResolver; /** * @readonly * @var \Rector\NodeNameResolver\NodeNameResolver */ private $nodeNameResolver; /** * @readonly * @var \Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer */ private $phpAttributeAnalyzer; /** * @readonly * @var \Rector\NodeTypeResolver\NodeTypeResolver */ private $nodeTypeResolver; /** * @readonly * @var \Rector\Php80\NodeAnalyzer\PromotedPropertyResolver */ private $promotedPropertyResolver; /** * @readonly * @var \Rector\TypeDeclaration\AlreadyAssignDetector\ConstructorAssignDetector */ private $constructorAssignDetector; /** * @readonly * @var \Rector\Core\PhpParser\AstResolver */ private $astResolver; /** * @readonly * @var \Rector\Core\NodeAnalyzer\PropertyFetchAnalyzer */ private $propertyFetchAnalyzer; /** * @readonly * @var \Rector\Core\Util\MultiInstanceofChecker */ private $multiInstanceofChecker; public function __construct(\Rector\Core\NodeManipulator\AssignManipulator $assignManipulator, BetterNodeFinder $betterNodeFinder, VariableToConstantGuard $variableToConstantGuard, ReadWritePropertyAnalyzer $readWritePropertyAnalyzer, PhpDocInfoFactory $phpDocInfoFactory, PropertyFetchFinder $propertyFetchFinder, ReflectionResolver $reflectionResolver, NodeNameResolver $nodeNameResolver, PhpAttributeAnalyzer $phpAttributeAnalyzer, NodeTypeResolver $nodeTypeResolver, PromotedPropertyResolver $promotedPropertyResolver, ConstructorAssignDetector $constructorAssignDetector, AstResolver $astResolver, PropertyFetchAnalyzer $propertyFetchAnalyzer, MultiInstanceofChecker $multiInstanceofChecker) { $this->assignManipulator = $assignManipulator; $this->betterNodeFinder = $betterNodeFinder; $this->variableToConstantGuard = $variableToConstantGuard; $this->readWritePropertyAnalyzer = $readWritePropertyAnalyzer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->propertyFetchFinder = $propertyFetchFinder; $this->reflectionResolver = $reflectionResolver; $this->nodeNameResolver = $nodeNameResolver; $this->phpAttributeAnalyzer = $phpAttributeAnalyzer; $this->nodeTypeResolver = $nodeTypeResolver; $this->promotedPropertyResolver = $promotedPropertyResolver; $this->constructorAssignDetector = $constructorAssignDetector; $this->astResolver = $astResolver; $this->propertyFetchAnalyzer = $propertyFetchAnalyzer; $this->multiInstanceofChecker = $multiInstanceofChecker; } /** * @param \PhpParser\Node\Stmt\Property|\PhpParser\Node\Param $propertyOrParam */ public function isPropertyChangeableExceptConstructor(Class_ $class, $propertyOrParam, Scope $scope) : bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($class); if ($this->hasAllowedNotReadonlyAnnotationOrAttribute($phpDocInfo, $class)) { return \true; } $propertyFetches = $this->propertyFetchFinder->findPrivatePropertyFetches($class, $propertyOrParam); foreach ($propertyFetches as $propertyFetch) { if ($this->isChangeableContext($propertyFetch, $scope)) { return \true; } // skip for constructor? it is allowed to set value in constructor method $propertyName = (string) $this->nodeNameResolver->getName($propertyFetch); $classMethod = $this->betterNodeFinder->findParentType($propertyFetch, ClassMethod::class); if ($this->isPropertyAssignedOnlyInConstructor($class, $propertyName, $classMethod)) { continue; } if ($this->assignManipulator->isLeftPartOfAssign($propertyFetch)) { return \true; } $isInUnset = (bool) $this->betterNodeFinder->findParentType($propertyFetch, Unset_::class); if ($isInUnset) { return \true; } } return \false; } /** * @api Used in rector-symfony */ public function resolveExistingClassPropertyNameByType(Class_ $class, ObjectType $objectType) : ?string { foreach ($class->getProperties() as $property) { $propertyType = $this->nodeTypeResolver->getType($property); if (!$propertyType->equals($objectType)) { continue; } return $this->nodeNameResolver->getName($property); } $promotedPropertyParams = $this->promotedPropertyResolver->resolveFromClass($class); foreach ($promotedPropertyParams as $promotedPropertyParam) { $paramType = $this->nodeTypeResolver->getType($promotedPropertyParam); if (!$paramType->equals($objectType)) { continue; } return $this->nodeNameResolver->getName($promotedPropertyParam); } return null; } public function isUsedByTrait(ClassReflection $classReflection, string $propertyName) : bool { foreach ($classReflection->getTraits() as $traitUse) { $trait = $this->astResolver->resolveClassFromName($traitUse->getName()); if (!$trait instanceof Trait_) { continue; } if ($this->propertyFetchAnalyzer->containsLocalPropertyFetchName($trait, $propertyName)) { return \true; } } return \false; } private function isPropertyAssignedOnlyInConstructor(Class_ $class, string $propertyName, ?ClassMethod $classMethod) : bool { if (!$classMethod instanceof ClassMethod) { return \false; } // there is property unset in Test class, so only check on __construct if (!$this->nodeNameResolver->isName($classMethod->name, MethodName::CONSTRUCT)) { return \false; } return $this->constructorAssignDetector->isPropertyAssigned($class, $propertyName); } /** * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ private function isChangeableContext($propertyFetch, Scope $scope) : bool { $parentNode = $propertyFetch->getAttribute(AttributeKey::PARENT_NODE); if (!$parentNode instanceof Node) { return \false; } if ($this->multiInstanceofChecker->isInstanceOf($parentNode, [PreInc::class, PreDec::class, PostInc::class, PostDec::class])) { $parentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE); } if (!$parentNode instanceof Node) { return \false; } if ($parentNode instanceof Arg) { $readArg = $this->variableToConstantGuard->isReadArg($parentNode); if (!$readArg) { return \true; } $callerNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE); if ($callerNode instanceof MethodCall || $callerNode instanceof StaticCall) { return $this->isFoundByRefParam($callerNode, $scope); } } if ($parentNode instanceof ArrayDimFetch) { return !$this->readWritePropertyAnalyzer->isRead($propertyFetch, $scope); } return $parentNode instanceof Unset_; } /** * @param \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $node */ private function isFoundByRefParam($node, Scope $scope) : bool { $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); if ($functionLikeReflection === null) { return \false; } $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionLikeReflection, $node, $scope); foreach ($parametersAcceptor->getParameters() as $parameterReflection) { if ($parameterReflection->passedByReference()->yes()) { return \true; } } return \false; } private function hasAllowedNotReadonlyAnnotationOrAttribute(PhpDocInfo $phpDocInfo, Class_ $class) : bool { if ($phpDocInfo->hasByAnnotationClasses(self::ALLOWED_NOT_READONLY_ANNOTATION_CLASS_OR_ATTRIBUTES)) { return \true; } return $this->phpAttributeAnalyzer->hasPhpAttributes($class, self::ALLOWED_NOT_READONLY_ANNOTATION_CLASS_OR_ATTRIBUTES); } }