NodeTypeResolver   C
last analyzed

Complexity

Total Complexity 53

Size/Duplication

Total Lines 225
Duplicated Lines 10.67 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 16
dl 24
loc 225
rs 5.5179
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B getType() 0 14 8
A getLastChainNodeType() 0 5 1
C getChainType() 0 59 16
D createChain() 0 33 15
A getVarType() 0 8 2
A getMethodType() 0 15 4
A getPropertyType() 12 12 3
A getParentType() 12 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like NodeTypeResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NodeTypeResolver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Padawan\Framework\Complete\Resolver;
4
5
use Padawan\Domain\Event\TypeResolveEvent;
6
use Padawan\Domain\Project\Index;
7
use Padawan\Domain\Project\FQCN;
8
use Padawan\Domain\Project\FQN;
9
use Padawan\Domain\Scope;
10
use Padawan\Parser\UseParser;
11
use PhpParser\Node\Name;
12
use PhpParser\Node\Expr\Variable;
13
use PhpParser\Node\Expr\PropertyFetch;
14
use PhpParser\Node\Expr\StaticPropertyFetch;
15
use PhpParser\Node\Expr\StaticCall;
16
use PhpParser\Node\Expr\MethodCall;
17
use PhpParser\Node\Expr\FuncCall;
18
use PhpParser\Node\Expr\New_;
19
use PhpParser\Node\Expr\Assign;
20
use Psr\Log\LoggerInterface;
21
use Padawan\Domain\Project\Chain;
22
use Padawan\Domain\Project\Chain\MethodCall as ChainMethodCall;
23
use Symfony\Component\EventDispatcher\EventDispatcher;
24
25
class NodeTypeResolver
26
{
27
28
    const BLOCK_START = 'type.block.before';
29
    const BLOCK_END = 'type.block.after';
30
    const TYPE_RESOLVED = 'type.resolving.after';
31
32
    public function __construct(
33
        LoggerInterface $logger,
34
        UseParser $useParser,
35
        EventDispatcher $dispatcher
36
    ) {
37
        $this->logger = $logger;
38
        $this->useParser = $useParser;
39
        $this->dispatcher = $dispatcher;
40
    }
41
42
    /**
43
     * Calculates type of the passed node
44
     *
45
     * @param \PhpParser\Node\Expr $node
46
     * @param Index $index
47
     * @param Scope $scope
48
     * @return FQCN|null
49
     */
50
    public function getType($node, Index $index, Scope $scope)
51
    {
52
        if ($node instanceof Variable
53
            || $node instanceof PropertyFetch
54
            || $node instanceof StaticPropertyFetch
55
            || $node instanceof MethodCall
56
            || $node instanceof StaticCall
57
        ) {
58
            return $this->getLastChainNodeType($node, $index, $scope);
59
        } elseif ($node instanceof New_ && $node->class instanceof Name) {
60
            return $this->useParser->getFQCN($node->class);
61
        }
62
        return null;
63
    }
64
65
    /**
66
     * Calculates type of the passed last node in chain
67
     *
68
     * @param \PhpParser\Node $node
69
     * @param Index $index
70
     * @param Scope $scope
71
     * @return FQCN|null
72
     */
73
    public function getLastChainNodeType($node, Index $index, Scope $scope)
74
    {
75
        $types = $this->getChainType($node, $index, $scope);
76
        return array_pop($types);
77
    }
78
79
    /**
80
     * Calculates type of the passed chained node
81
     *
82
     * @param \PhpParser\Node $node
83
     * @param Index $index
84
     * @param Scope $scope
85
     * @return FQCN[]
86
     */
87
    public function getChainType($node, Index $index, Scope $scope)
88
    {
89
        /** @var FQCN */
90
        $type = null;
91
        $types = [];
92
        $chain = $this->createChain($node);
93
        $block = $chain;
94
        while ($block instanceof Chain) {
95
            $this->logger->debug('looking for type of ' . $block->getName());
96
            $event = new TypeResolveEvent($block, $type);
97
            $this->dispatcher->dispatch(self::BLOCK_START, $event);
98
            if ($block->getType() === 'var') {
99
                $type = $this->getVarType($block->getName(), $scope);
100
            } elseif ($block->getType() === 'method') {
101
                if (!($type instanceof FQN)) {
102
                    $types[] = null;
103
                    break;
104
                }
105
                $type = $this->getMethodType($block->getName(), $type, $index);
106
            } elseif ($block->getType() === 'property') {
107
                if (!($type instanceof FQN)) {
108
                    $types[] = null;
109
                    break;
110
                }
111
                $type = $this->getPropertyType($block->getName(), $type, $index);
112
            } elseif ($block->getType() === 'class') {
113
                $type = $block->getName();
114
                if ($type instanceof FQCN) {
115
                    if ($type instanceof FQCN && (
116
                        $type->getClassName() === 'self'
117
                        || $type->getClassName() === 'static'
118
                    )
119
                    ) {
120
                        $type = $scope->getFQCN();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Padawan\Domain\Scope as the method getFQCN() does only exist in the following implementations of said interface: Padawan\Domain\Scope\ClassScope, Padawan\Domain\Scope\MethodScope.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
121
                    } elseif ($type->getClassName() === 'parent'
122
                        && $scope->getFQCN() instanceof FQCN
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Padawan\Domain\Scope as the method getFQCN() does only exist in the following implementations of said interface: Padawan\Domain\Scope\ClassScope, Padawan\Domain\Scope\MethodScope.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
123
                    ) {
124
                        $type = $this->getParentType($scope->getFQCN(), $index);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Padawan\Domain\Scope as the method getFQCN() does only exist in the following implementations of said interface: Padawan\Domain\Scope\ClassScope, Padawan\Domain\Scope\MethodScope.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
125
                    }
126
                }
127
            } elseif ($block->getType() === 'function') {
128
                $name = $block->getName();
129
                $function = $index->findFunctionByName($name->toString());
0 ignored issues
show
Bug introduced by
The method toString cannot be called on $name (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
130
                if (empty($function)) {
131
                    $type = null;
132
                } else {
133
                    $type = $function->return;
134
                }
135
            }
136
            $event = new TypeResolveEvent($block, $type);
137
            $this->dispatcher->dispatch(self::BLOCK_END, $event);
138
            $type = $event->getType();
139
            $block = $block->getChild();
140
            $types[] = $type;
141
        }
142
        $event = new TypeResolveEvent($chain, $type);
143
        $this->dispatcher->dispatch(self::TYPE_RESOLVED, $event);
144
        return $types;
145
    }
146
147
    /**
148
     * @return Chain
149
     */
150
    protected function createChain($node)
151
    {
152
        $chain = null;
153
        if ($node instanceof Assign) {
154
            $node = $node->expr;
155
        }
156
        while (!($node instanceof Variable) && !($node instanceof FuncCall)) {
157
            if ($node instanceof PropertyFetch
158
                || $node instanceof StaticPropertyFetch
159
            ) {
160
                $chain = new Chain($chain, $node->name, 'property');
161
            } elseif ($node instanceof MethodCall
162
                || $node instanceof StaticCall
163
            ) {
164
                $chain = new ChainMethodCall($chain, $node->name, $node->args);
165
            }
166
            if (empty($node) || !property_exists($node, 'var')) {
167
                break;
168
            }
169
            $node = $node->var;
170
        }
171
        if (!empty($node) && property_exists($node, 'class')) {
172
            $node = $node->class;
173
        }
174
        if ($node instanceof Variable) {
175
            $chain = new Chain($chain, $node->name, 'var');
0 ignored issues
show
Bug introduced by
It seems like $node->name can also be of type object<PhpParser\Node\Expr>; however, Padawan\Domain\Project\Chain::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
176
        } elseif ($node instanceof Name) {
177
            $chain = new Chain($chain, $this->useParser->getFQCN($node), 'class');
0 ignored issues
show
Documentation introduced by
$this->useParser->getFQCN($node) is of type null|object<Padawan\Domain\Project\FQCN>|boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
178
        } elseif ($node instanceof FuncCall) {
179
            $chain = new Chain($chain, $node->name, 'function');
0 ignored issues
show
Documentation introduced by
$node->name is of type object<PhpParser\Node\Na...ct<PhpParser\Node\Expr>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
180
        }
181
        return $chain;
182
    }
183
184
    /**
185
     * @param string $name
186
     */
187
    protected function getVarType($name, Scope $scope)
188
    {
189
        $var = $scope->getVar($name);
190
        if (empty($var)) {
191
            return null;
192
        }
193
        return $var->getType();
194
    }
195
196
    /**
197
     * @param string $name
198
     */
199
    protected function getMethodType($name, FQCN $type, Index $index)
200
    {
201
        $class = $index->findClassByFQCN($type);
202
        if (empty($class)) {
203
            $class = $index->findInterfaceByFQCN($type);
204
        }
205
        if (empty($class)) {
206
            return null;
207
        }
208
        $method = $class->methods->get($name);
209
        if (empty($method)) {
210
            return null;
211
        }
212
        return $method->getReturn();
213
    }
214
215
    /**
216
     * @param string $name
217
     */
218 View Code Duplication
    protected function getPropertyType($name, FQCN $type, Index $index)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
    {
220
        $class = $index->findClassByFQCN($type);
221
        if (empty($class)) {
222
            return null;
223
        }
224
        $prop = $class->properties->get($name);
0 ignored issues
show
Documentation introduced by
The property $properties is declared private in Padawan\Domain\Project\Node\ClassData. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
225
        if (empty($prop)) {
226
            return null;
227
        }
228
        return $prop->getType();
229
    }
230 View Code Duplication
    protected function getParentType(FQCN $type, Index $index)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
    {
232
        $class = $index->findClassByFQCN($type);
233
        if (empty($class)) {
234
            return null;
235
        }
236
        $parent = $class->getParent();
237
        if (empty($parent)) {
238
            return null;
239
        }
240
        return $parent->fqcn;
241
    }
242
243
    /** @property LoggerInterface */
244
    private $logger;
245
    /** @property UseParser */
246
    private $useParser;
247
    /** @var EventDispatcher */
248
    private $dispatcher;
249
}
250