ScopeWalker::parseUse()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 3
1
<?php
2
3
namespace Padawan\Parser\Walker;
4
5
use Padawan\Parser\UseParser;
6
use Padawan\Parser\CommentParser;
7
use Padawan\Parser\ParamParser;
8
use Padawan\Framework\Complete\Resolver\NodeTypeResolver;
9
use Padawan\Domain\Project\FQCN;
10
use Padawan\Domain\Project\Index;
11
use Padawan\Domain\Project\Node\Uses;
12
use Padawan\Domain\Project\Node\Variable;
13
use Padawan\Domain\Scope;
14
use Padawan\Domain\Scope\FileScope;
15
use Padawan\Domain\Scope\FunctionScope;
16
use Padawan\Domain\Scope\MethodScope;
17
use Padawan\Domain\Scope\ClassScope;
18
use Padawan\Domain\Scope\ClosureScope;
19
use PhpParser\NodeTraverserInterface;
20
use PhpParser\NodeVisitorAbstract;
21
use PhpParser\Node;
22
use PhpParser\Node\Expr\Variable as NodeVar;
23
use PhpParser\Node\Expr\Assign;
24
use PhpParser\Node\Stmt\Use_;
25
use PhpParser\Node\Stmt\Class_;
26
use PhpParser\Node\Stmt\ClassMethod;
27
use PhpParser\Node\Expr\Closure;
28
29
class ScopeWalker extends NodeVisitorAbstract implements WalkerInterface
30
{
31
    public function __construct(
32
        UseParser $useParser,
33
        NodeTypeResolver $typeResolver,
34
        CommentParser $commentParser,
35
        ParamParser $paramParser
36
    ) {
37
        $this->useParser        = $useParser;
38
        $this->typeResolver     = $typeResolver;
39
        $this->commentParser    = $commentParser;
40
        $this->paramParser      = $paramParser;
41
    }
42
    public function setLine($line)
43
    {
44
        $this->line = $line;
45
    }
46
    public function enterNode(Node $node)
47
    {
48
        list($startLine, $endLine) = $this->getNodeLines($node);
0 ignored issues
show
Unused Code introduced by
The assignment to $startLine is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $endLine is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
49
        if (!$this->isIn($node, $this->line)) {
50
            return NodeTraverserInterface::DONT_TRAVERSE_CHILDREN;
51
        }
52
        if ($node instanceof Class_) {
53
            $this->createScopeFromClass($node);
54
        } elseif ($node instanceof ClassMethod) {
55
            $this->createScopeFromMethod($node);
56
        } elseif ($node instanceof Closure) {
57
            $this->createScopeFromClosure($node);
58
        } elseif ($node instanceof Assign) {
59
            $this->addVarToScope($node);
60
        }
61
    }
62
    public function leaveNode(Node $node)
63
    {
64
        if (!$this->isIn($node, $this->line)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
65
        }
66
    }
67
    public function updateFileInfo(Uses $uses, $file)
68
    {
69
        $this->scope = new FileScope($uses->getFQCN(), $uses);
0 ignored issues
show
Bug introduced by
It seems like $uses->getFQCN() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
70
        $this->fileScope = $this->scope;
71
    }
72
    public function getResultScope()
73
    {
74
        return $this->scope;
75
    }
76
77
    /**
78
     * @param Node $node
79
     */
80
    public function isIn($node, $line)
81
    {
82
        list($startLine, $endLine) = $this->getNodeLines($node);
83
        if ($node instanceof ClassMethod
84
            || $node instanceof Closure
85
            || $node instanceof Class_
86
        ) {
87
            return $line >= $startLine && $line <= $endLine;
88
        }
89
        return $line >= $startLine;
90
    }
91
    public function getNodeLines($node)
92
    {
93
        $startLine = $endLine = -1;
94
        if ($node->hasAttribute('startLine')) {
95
            $startLine = $node->getAttribute('startLine');
96
        }
97
        if ($node->hasAttribute('endLine')) {
98
            $endLine = $node->getAttribute('endLine');
99
        }
100
        return [$startLine, $endLine];
101
    }
102
    protected function createScopeFromClass(Class_ $node)
103
    {
104
        $scope = $this->scope;
105
        $index = $this->getIndex();
106
        if (empty($index)) {
107
            return;
108
        }
109
        $fqcn = new FQCN(
110
            $node->name,
111
            $this->scope->getNamespace()
0 ignored issues
show
Documentation introduced by
$this->scope->getNamespace() is of type object<Padawan\Domain\Project\FQCN>|null, 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...
112
        );
113
        $classData = $index->findClassByFQCN($fqcn);
114
        if (empty($classData)) {
115
            return;
116
        }
117
        $this->scope = new ClassScope($scope, $classData);
118
    }
119
    public function createScopeFromClosure(Closure $node)
120
    {
121
        $scope = $this->scope;
122
        $this->scope = new ClosureScope($scope);
123
        foreach ($node->params as $param) {
124
            $this->scope->addVar(
125
                $this->paramParser->parse($param)
126
            );
127
        }
128
        foreach ($node->uses as $closureUse) {
129
            $var = $this->scope->getParent()->getVar($closureUse->var);
130
            if ($var instanceof Variable) {
131
                $this->scope->addVar(
132
                    $var
133
                );
134
            }
135
        }
136
    }
137
    public function createScopeFromMethod(ClassMethod $node)
138
    {
139
        $classScope = $this->scope;
140
        $index = $this->getIndex();
141
        if (empty($index)) {
142
            return;
143
        }
144
        $fqcn = $classScope->getFQCN();
0 ignored issues
show
Bug introduced by
The method getFQCN does only exist in Padawan\Domain\Scope\ClassScope, but not in Padawan\Domain\Scope\Clo...\Domain\Scope\FileScope.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
145
        $classData = $index->findClassByFQCN($fqcn);
146
        if (empty($classData)) {
147
            return;
148
        }
149
        $method = $classData->methods->get($node->name);
0 ignored issues
show
Documentation introduced by
The property $methods 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...
150
        if (empty($method)) {
151
            return;
152
        }
153
        $this->scope = new MethodScope($classScope, $method);
154
    }
155
    public function addVarToScope(Assign $node)
156
    {
157
        if (!$node->var instanceof NodeVar) {
158
            return;
159
        }
160
        $var = new Variable($node->var->name);
161
        $comment = $this->commentParser->parse($node->getAttribute('comments'));
162
        if ($comment->getVar($var->getName())) {
163
            $type = $comment->getVar($var->getName())->getType();
164
        } else {
165
            $type = $this->typeResolver->getType(
166
                $node->expr,
167
                $this->getIndex(),
168
                $this->scope
169
            );
170
        }
171
        $var->setType($type);
172
        $this->scope->addVar($var);
173
    }
174
    public function parseUse(Use_ $node, $fqcn, $file)
175
    {
176
        $this->useParser->parse($node, $fqcn, $file);
0 ignored issues
show
Unused Code introduced by
The call to UseParser::parse() has too many arguments starting with $fqcn.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
177
    }
178
179
    /**
180
     * @return FQCN
181
     */
182
    public function parseFQCN($fqcn)
183
    {
184
        return $this->useParser->parseFQCN($fqcn);
185
    }
186
187
    /**
188
     * @return Index
189
     */
190
    public function getIndex()
191
    {
192
        return $this->index;
193
    }
194
    public function setIndex(Index $index)
195
    {
196
        $this->index = $index;
197
    }
198
199
    /** @var FileScope */
200
    private $fileScope;
201
    private $line;
202
    /** @var UseParser */
203
    private $useParser;
204
    private $index;
205
    /** @property Scope */
206
    private $scope;
207
    private $typeResolver;
208
    private $commentParser;
209
    /** @property ParamParser */
210
    private $paramParser;
211
}
212