Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
NameResolver
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
4 / 4
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 enterNode
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
11
 normalizeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSpecialName
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Node visitor to resolve class names in the docs.
4 *
5 * @package automattic/jetpack-stub-generator
6 */
7
8namespace Automattic\Jetpack\StubGenerator\PhpDocParser;
9
10use PhpParser\Builder\Property as PropertyBuilder;
11use PhpParser\NameContext;
12use PhpParser\Node\Name;
13use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor;
14use PHPStan\PhpDocParser\Ast\Node;
15use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
16use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
17use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode;
18use Symfony\Component\Console\Output\OutputInterface;
19
20/**
21 * Node visitor to resolve class names in the docs.
22 *
23 * The expectation here is that there's a \PhpParser\NodeVisitor which runs this over the phpdoc comment of each visited
24 * node, and that visitor is itself called after a \PhpParser\NodeVisitor\NameResolver that's using the same
25 * NameContext as was passed to this.
26 *
27 * If the NameContext is currently processing a non-empty namespace, then non-fully-qualified class names are rewritten
28 * to be either fully-qualified or relative to the NameContext's namespace rather than making use of any `use` aliases.
29 *
30 * The "relative to" bit semi-accidentally avoids mangling unrecognized class names, `@phan-type` aliases, and the like
31 * most of the time, since the NameContext will probably "resolve" them relative to the namespace then the relative-to
32 * transform will turn them back.
33 */
34class NameResolver extends AbstractNodeVisitor {
35
36    /**
37     * OutputInterface.
38     *
39     * @var OutputInterface
40     */
41    private $output;
42
43    /**
44     * NameContext from the \PhpParser\NodeVisitor\NameResolver this is coordinating with.
45     *
46     * @var NameContext
47     */
48    private $nameContext;
49
50    /**
51     * Constructor.
52     *
53     * @param NameContext     $nameContext NameContext from the \PhpParser\NodeVisitor\NameResolver this is coordinating with.
54     * @param OutputInterface $output OutputInterface.
55     */
56    public function __construct( NameContext $nameContext, OutputInterface $output ) {
57        $this->nameContext = $nameContext;
58        $this->output      = $output;
59    }
60
61    // phpcs:ignore Squiz.Commenting.FunctionComment.Missing -- Inherited.
62    public function enterNode( Node $node ) {
63        // Flag array/object shape keys to not be resolved as class names.
64        if ( ( $node instanceof ArrayShapeItemNode || $node instanceof ObjectShapeItemNode ) && $node->keyName instanceof IdentifierTypeNode ) {
65            $node->keyName->setAttribute( 'not_a_class_name', true );
66        }
67
68        $ns = $this->nameContext->getNamespace();
69        if ( $ns && $node instanceof IdentifierTypeNode && ! $node->getAttribute( 'not_a_class_name' ) ) {
70            $name = self::normalizeType( $node->name );
71            if ( $name instanceof Name && ! $name->isFullyQualified() && ! $this->isSpecialName( $name->name ) ) {
72                $name = $this->nameContext->getResolvedClassName( $name );
73                if ( strpos( strtolower( $name ), strtolower( $ns . '\\' ) ) === 0 ) {
74                    $name = new Name( substr( $name, strlen( $ns ) + 1 ) );
75                }
76                $node->name = $name->toCodeString();
77            }
78        }
79    }
80
81    /**
82     * Normalize a type string.
83     *
84     * Unfortunately PhpParser\BuilderHelpers is marked internal, so we have to
85     * hack around it instead.
86     *
87     * @param string $type Type.
88     * @return ?\PhpParser\Node Result.
89     */
90    private static function normalizeType( string $type ) {
91        return ( new PropertyBuilder( 'dummy' ) )->setType( $type )->getNode()->type;
92    }
93
94    /**
95     * Guess whether a string seems to be a special name.
96     *
97     * @param string $name Name.
98     * @return bool
99     */
100    public function isSpecialName( string $name ): bool {
101        return in_array(
102            $name,
103            array(
104                'associative-array',
105                'callable-array',
106                'callable-object',
107                'callable-string',
108                'class-string',
109                'list',
110                'non-empty-array',
111                'non-empty-associative-array',
112                'non-empty-list',
113                'resource',
114            ),
115            true
116        );
117    }
118}