Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.24% covered (success)
95.24%
60 / 63
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddMissingMethodPhpDocRector
95.24% covered (success)
95.24%
60 / 63
66.67% covered (warning)
66.67%
4 / 6
28
0.00% covered (danger)
0.00%
0 / 1
 getRuleDefinition
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getNodeTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 refactor
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 resolveThrows
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 resolveNameToString
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 resolveTypeToString
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
9.11
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Rector;
21
22use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
23use PhpParser\Comment\Doc;
24use PhpParser\Node;
25use PhpParser\Node\ComplexType;
26use PhpParser\Node\Expr\New_;
27use PhpParser\Node\Identifier;
28use PhpParser\Node\IntersectionType;
29use PhpParser\Node\Name;
30use PhpParser\Node\NullableType;
31use PhpParser\Node\Stmt\ClassMethod;
32use PhpParser\Node\Expr\Throw_;
33use PhpParser\Node\UnionType;
34use PhpParser\NodeFinder;
35use Rector\Rector\AbstractRector;
36use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
37
38/**
39 * Executes AST inspections parsing missing documentation on methods automatically.
40 * It MUST append `@param`, `@return`, and `@throws` tags where deduced accurately.
41 * The logic SHALL NOT override existing documentation.
42 */
43 class AddMissingMethodPhpDocRector extends AbstractRector
44{
45    /**
46     * Delivers the formal rule description configured within the Rector ecosystem.
47     *
48     * The method MUST accurately describe its functional changes logically.
49     *
50     * @return RuleDefinition explains the rule's active behavior context
51     */
52    public function getRuleDefinition(): RuleDefinition
53    {
54        return new RuleDefinition('Add basic PHPDoc to methods without docblock', [
55            new CodeSample('public function foo() {}', "/**\n * \n */\npublic function foo() {}"),
56        ]);
57    }
58
59    /**
60     * Designates the primary Abstract Syntax Tree (AST) node structures intercepted.
61     *
62     * The method MUST register solely `ClassMethod` class references to guarantee precision.
63     *
64     * @return array<int, class-string<Node>> the structural bindings applicable for this modification
65     */
66    public function getNodeTypes(): array
67    {
68        return [ClassMethod::class];
69    }
70
71    /**
72     * Computes necessary PHPDoc metadata for a given class method selectively.
73     *
74     * The method MUST identify the missing `@param`, `@return`, and `@throws` tags algorithmically.
75     * It SHALL preserve pre-existing valid tags cleanly. If no augmentation is achieved, it returns the node unaltered.
76     *
77     * @param Node $node the target method representation parsed synchronously
78     *
79     * @return Node the refined active syntax instance inclusive of generated documentation
80     */
81    public function refactor(Node $node): Node
82    {
83        if (! $node instanceof ClassMethod) {
84            return $node;
85        }
86
87        $docComment = $node->getDocComment();
88
89        if ($docComment instanceof Doc) {
90            return $node;
91        }
92
93        $newTags = [];
94        foreach ($node->params as $param) {
95            $paramName = $this->getName($param->var);
96
97            if (null === $paramName) {
98                continue;
99            }
100
101            $newTags[] = \sprintf(' * @param %s $%s', $this->resolveTypeToString($param->type), $paramName);
102        }
103
104        if ('__construct' !== $node->name->toString()) {
105            if ([] !== $newTags) {
106                $newTags[] = ' *';
107            }
108
109            $newTags[] = \sprintf(' * @return %s', $this->resolveTypeToString($node->returnType));
110        }
111
112        foreach ($this->resolveThrows($node) as $exception) {
113            if ([] !== $newTags) {
114                $newTags[] = ' *';
115            }
116
117            $newTags[] = \sprintf(' * @throws %s', $exception);
118        }
119
120        if ([] === $newTags) {
121            return $node;
122        }
123
124        $docBlock = "/**\n" . implode("\n", $newTags) . "\n */";
125
126        $node->setDocComment(new Doc($docBlock));
127
128        return $node;
129    }
130
131    /**
132     * Parses the architectural scope of an intercepted method to infer exceptional operations natively.
133     *
134     * This method MUST accurately deduce exception creations traversing internal components recursively.
135     * It SHALL strictly return precise, unique internal naming identifiers safely.
136     *
137     * @param ClassMethod $node the active evaluated root target element dynamically instantiated
138     *
139     * @return string[] expected failure objects effectively defined within its contextual boundary
140     */
141    private function resolveThrows(ClassMethod $node): array
142    {
143        if (null === $node->stmts) {
144            return [];
145        }
146
147        $nodeFinder = new NodeFinder();
148
149        /** @var Throw_[] $throwNodes */
150        $throwNodes = $nodeFinder->findInstanceOf($node->stmts, Throw_::class);
151
152        $exceptions = [];
153
154        foreach ($throwNodes as $throwNode) {
155            $throwExpr = $throwNode->expr;
156
157            if (! $throwExpr instanceof New_) {
158                continue;
159            }
160
161            if (! $throwExpr->class instanceof Name) {
162                continue;
163            }
164
165            $exceptions[] = $this->resolveNameToString($throwExpr->class);
166        }
167
168        return array_values(array_unique($exceptions));
169    }
170
171    /**
172     * Expands Name syntax objects into human-readable string descriptors universally.
173     *
174     * The method MUST handle aliases seamlessly or fallback to base names dependably.
175     *
176     * @param Name $name the structured reference to parse accurately
177     *
178     * @return string the computed class identifier successfully reconstructed
179     */
180    private function resolveNameToString(Name $name): string
181    {
182        $originalName = $name->getAttribute('originalName');
183
184        if ($originalName instanceof Name) {
185            return $originalName->toString();
186        }
187
188        return $name->getLast();
189    }
190
191    /**
192     * Translates complicated type primitives cleanly back into uniform string declarations consistently.
193     *
194     * The method MUST parse complex combinations including Intersections, Unions natively and securely.
195     *
196     * @param string|Identifier|Name|ComplexType|null $type the original metadata instance safely captured
197     *
198     * @return string the final interpreted designation string explicitly represented safely
199     */
200    private function resolveTypeToString(string|Identifier|Name|ComplexType|null $type): string
201    {
202        if (null === $type) {
203            return 'mixed';
204        }
205
206        if (\is_string($type)) {
207            return $type;
208        }
209
210        if ($type instanceof Identifier) {
211            return $type->toString();
212        }
213
214        if ($type instanceof Name) {
215            $originalName = $type->getAttribute('originalName');
216
217            if ($originalName instanceof Name) {
218                return $originalName->toString();
219            }
220
221            return $type->toString();
222        }
223
224        if ($type instanceof NullableType) {
225            return $this->resolveTypeToString($type->type) . '|null';
226        }
227
228        if ($type instanceof UnionType) {
229            return implode('|', array_map($this->resolveTypeToString(...), $type->types));
230        }
231
232        if ($type instanceof IntersectionType) {
233            return implode('&', array_map($this->resolveTypeToString(...), $type->types));
234        }
235
236        return 'mixed';
237    }
238}