promotedPropertyCandidateResolver = $promotedPropertyCandidateResolver; $this->variableRenamer = $variableRenamer; $this->varTagRemover = $varTagRemover; $this->paramAnalyzer = $paramAnalyzer; $this->phpDocTypeChanger = $phpDocTypeChanger; $this->makePropertyPromotionGuard = $makePropertyPromotionGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Change simple property init and assign to constructor promotion', [new ConfiguredCodeSample(<<<'CODE_SAMPLE' class SomeClass { public float $someVariable; public function __construct( float $someVariable = 0.0 ) { $this->someVariable = $someVariable; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { public function __construct( public float $someVariable = 0.0 ) { } } CODE_SAMPLE , [self::INLINE_PUBLIC => \false])]); } public function configure(array $configuration) : void { $this->inlinePublic = $configuration[self::INLINE_PUBLIC] ?? (bool) \current($configuration); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $promotionCandidates = $this->promotedPropertyCandidateResolver->resolveFromClass($node); if ($promotionCandidates === []) { return null; } /** @var ClassMethod $constructClassMethod */ $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($constructClassMethod); foreach ($promotionCandidates as $promotionCandidate) { // does property have some useful annotations? $property = $promotionCandidate->getProperty(); $param = $promotionCandidate->getParam(); if ($this->shouldSkipParam($param)) { continue; } if (!$this->makePropertyPromotionGuard->isLegal($node, $property, $param, $this->inlinePublic)) { continue; } $this->removeNode($property); $this->removeNode($promotionCandidate->getAssign()); $property = $promotionCandidate->getProperty(); $paramName = $this->getName($param); // rename also following calls $propertyName = $this->getName($property->props[0]); /** @var string $oldName */ $oldName = $this->getName($param->var); $this->variableRenamer->renameVariableInFunctionLike($constructClassMethod, $oldName, $propertyName, null); $paramTagValueNode = $classMethodPhpDocInfo->getParamTagValueByName($paramName); if (!$paramTagValueNode instanceof ParamTagValueNode) { $this->decorateParamWithPropertyPhpDocInfo($constructClassMethod, $property, $param, $paramName); } elseif ($paramTagValueNode->parameterName !== '$' . $propertyName) { $paramTagValueNode->parameterName = '$' . $propertyName; $paramTagValueNode->setAttribute(PhpDocAttributeKey::ORIG_NODE, null); } // property name has higher priority $paramName = $this->getName($property); $param->var = new Variable($paramName); $param->flags = $property->flags; // Copy over attributes of the "old" property $param->attrGroups = \array_merge($param->attrGroups, $property->attrGroups); $this->processNullableType($property, $param); $this->phpDocTypeChanger->copyPropertyDocToParam($constructClassMethod, $property, $param); } return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::PROPERTY_PROMOTION; } private function processNullableType(Property $property, Param $param) : void { if ($this->nodeTypeResolver->isNullableType($property)) { $objectType = $this->getType($property); $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($objectType, TypeKind::PARAM); } if ($param->default instanceof Expr && $this->valueResolver->isNull($param->default)) { $paramType = $this->getType($param); $param->type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramType, TypeKind::PARAM); } } private function decorateParamWithPropertyPhpDocInfo(ClassMethod $classMethod, Property $property, Param $param, string $paramName) : void { $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); $propertyPhpDocInfo->markAsChanged(); $param->setAttribute(AttributeKey::PHP_DOC_INFO, $propertyPhpDocInfo); // make sure the docblock is useful if ($param->type === null) { $varTagValueNode = $propertyPhpDocInfo->getVarTagValueNode(); if (!$varTagValueNode instanceof VarTagValueNode) { return; } $paramType = $this->staticTypeMapper->mapPHPStanPhpDocTypeToPHPStanType($varTagValueNode, $property); $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); $this->phpDocTypeChanger->changeParamType($classMethodPhpDocInfo, $paramType, $param, $paramName); } else { $paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); } $this->varTagRemover->removeVarPhpTagValueNodeIfNotComment($param, $paramType); } 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'); } }