promotedPropertyCandidateResolver = $promotedPropertyCandidateResolver; $this->variableRenamer = $variableRenamer; $this->paramAnalyzer = $paramAnalyzer; $this->propertyPromotionDocBlockMerger = $propertyPromotionDocBlockMerger; $this->makePropertyPromotionGuard = $makePropertyPromotionGuard; $this->typeComparator = $typeComparator; $this->reflectionResolver = $reflectionResolver; $this->propertyPromotionRenamer = $propertyPromotionRenamer; $this->phpDocInfoFactory = $phpDocInfoFactory; $this->staticTypeMapper = $staticTypeMapper; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change simple property init and assign to constructor promotion', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public float $price; public function __construct( float $price = 0.0 ) { $this->price = $price; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( public float $price = 0.0 ) { } } CODE_SAMPLE , [\Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector::INLINE_PUBLIC => \false, \Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector::RENAME_PROPERTY => \true])]); } public function configure(array $configuration) : void { $this->inlinePublic = $configuration[self::INLINE_PUBLIC] ?? \false; $this->renameProperty = $configuration[self::RENAME_PROPERTY] ?? \true; } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); if (!$constructClassMethod instanceof ClassMethod) { return null; } $promotionCandidates = $this->promotedPropertyCandidateResolver->resolveFromClass($node, $constructClassMethod); if ($promotionCandidates === []) { return null; } $constructorPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($constructClassMethod); $classReflection = $this->reflectionResolver->resolveClassReflection($node); if (!$classReflection instanceof ClassReflection) { return null; } foreach ($promotionCandidates as $promotionCandidate) { $param = $promotionCandidate->getParam(); if ($this->shouldSkipParam($param)) { continue; } $property = $promotionCandidate->getProperty(); if (!$this->makePropertyPromotionGuard->isLegal($node, $classReflection, $property, $param, $this->inlinePublic)) { continue; } $paramName = $this->getName($param); // rename also following calls $propertyName = $this->getName($property->props[0]); if (!$this->renameProperty && $paramName !== $propertyName) { continue; } // remove property from class $propertyStmtKey = $property->getAttribute(AttributeKey::STMT_KEY); unset($node->stmts[$propertyStmtKey]); // remove assign in constructor $assignStmtPosition = $promotionCandidate->getStmtPosition(); unset($constructClassMethod->stmts[$assignStmtPosition]); /** @var string $oldName */ $oldName = $this->getName($param->var); $this->variableRenamer->renameVariableInFunctionLike($constructClassMethod, $oldName, $propertyName, null); $paramTagValueNode = $constructorPhpDocInfo->getParamTagValueByName($paramName); if (!$paramTagValueNode instanceof ParamTagValueNode) { $this->propertyPromotionDocBlockMerger->decorateParamWithPropertyPhpDocInfo($constructClassMethod, $property, $param, $paramName); } elseif ($paramTagValueNode->parameterName !== '$' . $propertyName) { $this->propertyPromotionRenamer->renameParamDoc($constructorPhpDocInfo, $constructClassMethod, $param, $paramTagValueNode->parameterName, $propertyName); } // property name has higher priority $paramName = $this->getName($property); $param->var = new Variable($paramName); $param->flags = $property->flags; // copy attributes of the old property $param->attrGroups = \array_merge($param->attrGroups, $property->attrGroups); $this->processUnionType($property, $param); $this->propertyPromotionDocBlockMerger->mergePropertyAndParamDocBlocks($property, $param, $paramTagValueNode); // update variable to property fetch references $this->traverseNodesWithCallable((array) $constructClassMethod->stmts, function (Node $node) use($promotionCandidate, $propertyName) : ?PropertyFetch { if (!$node instanceof Variable) { return null; } if (!$this->isName($node, $promotionCandidate->getParamName())) { return null; } return new PropertyFetch(new Variable('this'), $propertyName); }); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PROPERTY_PROMOTION; } private function processUnionType(Property $property, Param $param) : void { if ($property->type instanceof Node) { $param->type = $property->type; return; } if (!$param->default instanceof Expr) { return; } if (!$param->type instanceof Node) { return; } $defaultType = $this->getType($param->default); $paramType = $this->getType($param->type); if ($this->typeComparator->isSubtype($defaultType, $paramType)) { return; } if ($this->typeComparator->areTypesEqual($defaultType, $paramType)) { return; } if ($paramType instanceof MixedType) { return; } $paramType = TypeCombinator::union($paramType, $defaultType); $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramType, TypeKind::PARAM); } private function shouldSkipParam(Param $param) : bool { if ($param->variadic) { return \true; } if ($this->paramAnalyzer->isNullable($param)) { /** @var NullableType $type */ $type = $param->type; $type = $type->type; } else { $type = $param->type; } if ($this->isCallableTypeIdentifier($type)) { return \true; } if (!$type instanceof UnionType) { return \false; } foreach ($type->types as $type) { if ($this->isCallableTypeIdentifier($type)) { return \true; } } return \false; } private function isCallableTypeIdentifier(?Node $node) : bool { return $node instanceof Identifier && $this->isName($node, 'callable'); } }