parentClassMethodTypeOverrideGuard = $parentClassMethodTypeOverrideGuard; $this->phpVersionProvider = $phpVersionProvider; $this->staticTypeMapper = $staticTypeMapper; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add missing return type declaration based on parent class method', [new CodeSample(<<<'CODE_SAMPLE' class A { public function execute(): int { } } class B extends A{ public function execute() { } } CODE_SAMPLE , <<<'CODE_SAMPLE' class A { public function execute(): int { } } class B extends A{ public function execute(): int { } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { $hasChanged = \false; foreach ($node->getMethods() as $classMethod) { if ($this->isName($classMethod, MethodName::CONSTRUCT)) { continue; } $parentClassMethodReturnType = $this->getReturnTypeRecursive($classMethod); if (!$parentClassMethodReturnType instanceof Type) { continue; } $changedClassMethod = $this->processClassMethodReturnType($node, $classMethod, $parentClassMethodReturnType); if (!$changedClassMethod instanceof ClassMethod) { continue; } $hasChanged = \true; } if ($hasChanged) { return $node; } return null; } private function getReturnTypeRecursive(ClassMethod $classMethod) : ?Type { $returnType = $classMethod->getReturnType(); if ($returnType !== null) { return $this->staticTypeMapper->mapPhpParserNodePHPStanType($returnType); } $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($classMethod); while ($parentMethodReflection instanceof MethodReflection) { if ($parentMethodReflection->isPrivate()) { return null; } $parameterAcceptor = ParametersAcceptorSelector::selectSingle($parentMethodReflection->getVariants()); if (!$parameterAcceptor instanceof ParametersAcceptorWithPhpDocs) { return null; } $parentReturnType = $parameterAcceptor->getNativeReturnType(); if (!$parentReturnType instanceof MixedType) { return $parentReturnType; } if ($parentReturnType->isExplicitMixed()) { return $parentReturnType; } $parentMethodReflection = $this->parentClassMethodTypeOverrideGuard->getParentClassMethod($parentMethodReflection); } return null; } private function processClassMethodReturnType(Class_ $class, ClassMethod $classMethod, Type $parentType) : ?ClassMethod { if ($parentType instanceof MixedType) { $className = (string) $this->nodeNameResolver->getName($class); $currentObjectType = new ObjectType($className); if (!$parentType->equals($currentObjectType) && $classMethod->returnType !== null) { return null; } } if ($parentType instanceof MixedType && !$this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { return null; } // already set and sub type or equal → no change if ($this->parentClassMethodTypeOverrideGuard->shouldSkipReturnTypeChange($classMethod, $parentType)) { return null; } $classMethod->returnType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($parentType, TypeKind::RETURN); return $classMethod; } }