familyRelationsAnalyzer = $familyRelationsAnalyzer; $this->returnTypeInferer = $returnTypeInferer; $this->classAnalyzer = $classAnalyzer; $this->betterNodeFinder = $betterNodeFinder; } public function provideMinPhpVersion() : int { return PhpVersionFeature::STRINGABLE; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add `Stringable` interface to classes with `__toString()` method', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function __toString() { return 'I can stringz'; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass implements Stringable { public function __toString(): string { return 'I can stringz'; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Class_::class]; } /** * @param Class_ $node */ public function refactor(Node $node) : ?Node { if ($this->classAnalyzer->isAnonymousClass($node)) { return null; } $toStringClassMethod = $node->getMethod(MethodName::TO_STRING); if (!$toStringClassMethod instanceof ClassMethod) { return null; } $this->hasChanged = \false; // warning, classes that implements __toString() will return Stringable interface even if they don't implemen it // reflection cannot be used for real detection $classLikeAncestorNames = $this->familyRelationsAnalyzer->getClassLikeAncestorNames($node); $isAncestorHasStringable = \in_array(self::STRINGABLE, $classLikeAncestorNames, \true); $returnType = $this->returnTypeInferer->inferFunctionLike($toStringClassMethod); if (!$returnType->isString()->yes()) { $this->processNotStringType($toStringClassMethod); } if (!$isAncestorHasStringable) { // add interface $node->implements[] = new FullyQualified(self::STRINGABLE); $this->hasChanged = \true; } // add return type if ($toStringClassMethod->returnType === null) { $toStringClassMethod->returnType = new Identifier('string'); $this->hasChanged = \true; } if (!$this->hasChanged) { return null; } return $node; } private function processNotStringType(ClassMethod $toStringClassMethod) : void { if ($toStringClassMethod->isAbstract()) { return; } $hasReturn = $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped($toStringClassMethod, Return_::class); if (!$hasReturn) { $emptyStringReturn = new Return_(new String_('')); $toStringClassMethod->stmts[] = $emptyStringReturn; $this->hasChanged = \true; return; } $this->traverseNodesWithCallable((array) $toStringClassMethod->stmts, function (Node $subNode) { if (!$subNode instanceof Return_) { return null; } if (!$subNode->expr instanceof Expr) { $subNode->expr = new String_(''); return null; } $type = $this->nodeTypeResolver->getType($subNode->expr); if ($type->isString()->yes()) { return null; } $subNode->expr = new CastString_($subNode->expr); $this->hasChanged = \true; return null; }); } }