ContextResolver::createContext()   C
last analyzed

Complexity

Conditions 13
Paths 26

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 5.1234
c 0
b 0
f 0
cc 13
eloc 27
nc 26
nop 4

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Padawan\Framework\Complete\Resolver;
4
5
use Padawan\Domain\Completion\Token;
6
use Padawan\Domain\Completion\Context;
7
use Padawan\Domain\Scope;
8
use Padawan\Domain\Scope\FileScope;
9
use Padawan\Domain\Project\FQN;
10
use Padawan\Domain\Project\Index;
11
use Padawan\Domain\Project\FQCN;
12
use Padawan\Parser\ErrorFreePhpParser;
13
use Padawan\Parser\UseParser;
14
use Psr\Log\LoggerInterface;
15
use PhpParser\Node\Expr\Variable;
16
use PhpParser\Node\Name;
17
18
class ContextResolver
19
{
20
    public function __construct(
21
        ErrorFreePhpParser $parser,
22
        NodeTypeResolver $typeResolver,
23
        LoggerInterface $logger,
24
        UseParser $useParser
25
    ) {
26
        $this->parser = $parser;
27
        $this->typeResolver = $typeResolver;
28
        $this->logger = $logger;
29
        $this->useParser = $useParser;
30
    }
31
    public function getContext($badLine, Index $index, Scope $scope = null)
32
    {
33
        if (empty($scope)) {
34
            $scope = new FileScope(new FQN);
35
        }
36
37
        $token = $this->getLastToken($badLine);
38
        $this->logger->debug(sprintf(
39
            'Found token \'%s\' with type %s',
40
            $token->getSymbol(),
41
            $token->getType()
42
        ));
43
        return $this->createContext($scope, $token, $badLine, $index);
44
    }
45
46
    /**
47
     * @return Token
48
     */
49
    protected function getLastToken($badLine)
50
    {
51
        try {
52
            $symbols = @token_get_all($this->prepareLine($badLine, false));
53
        } catch (\Exception $e) {
54
            $symbols = [0, 0];
55
        }
56
        $token = null;
57
        array_shift($symbols);
58
        do {
59
            $token = $this->addSymbolForToken(array_pop($symbols), $token);
60
        } while (!$token->isReady() && count($symbols));
61
        return $token;
62
    }
63
64
    protected function createContext(Scope $scope, Token $token, $badLine, Index $index)
65
    {
66
        $context = new Context($scope, $token);
67
        $nodes = $this->parser->parse($this->prepareLine($badLine));
68
69
        if ($token->isObjectOperator() || $token->isStaticOperator() || $token->isMethodCall()) {
70
            if (is_array($nodes)) {
71
                $workingNode = array_pop($nodes);
72
            } else {
73
                $workingNode = $nodes;
74
            }
75
            $isThis = false;
76
            if ($workingNode instanceof Variable && $workingNode->name === 'this') {
77
                $isThis = true;
78
            }
79
            if ($workingNode instanceof Name) {
80
                $nodeFQCN = $this->useParser->getFQCN($workingNode);
81
                if ($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...
82
                    && $nodeFQCN->toString() === $scope->getFQCN()->toString()
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...
83
                ) {
84
                    $isThis = true;
85
                }
86
            }
87
            $types = $this->typeResolver->getChainType($workingNode, $index, $scope);
88
            $context->setData([
89
                array_pop($types),
90
                $isThis,
91
                $types,
92
                $workingNode
93
            ]);
94
        }
95
        if ($token->isUseOperator()
96
            || $token->isNamespaceOperator()
97
            || $token->isNewOperator()
98
        ) {
99
            $context->setData(trim($token->getSymbol()));
100
        }
101
102
        return $context;
103
    }
104
105
    protected function addSymbolForToken($symbol, Token $token = null)
106
    {
107
        if (is_array($symbol)) {
108
            $code = $symbol[0];
109
            $symbol = $symbol[1];
110
        } else {
111
            $code = $symbol;
112
        }
113
        if (empty($token)) {
114
            $token = new Token($code, $symbol);
115
        } else {
116
            $token->add($code, $symbol);
117
        }
118
        return $token;
119
    }
120
121
    protected function prepareLine($badLine, $wrapFunctionCall = true)
122
    {
123
        if (strpos($badLine, '<?php') === false
124
            || strpos($badLine, '<?') === false
125
        ) {
126
            $badLine = '<?php ' . $badLine;
127
        }
128
        $badLine = str_replace(['elseif', 'else', 'catch'], '', $badLine);
129
        if ($wrapFunctionCall && $badLine[strlen($badLine) - 1] === '(') {
130
            $badLine .= ')';
131
        }
132
        return $badLine;
133
    }
134
135
    private $logger;
136
    private $parser;
137
    private $typeResolver;
138
    private $useParser;
139
}
140