Completed
Push — master ( a15224...c4d2d4 )
by Alexander
9s
created

src/ReflectionParameter.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Parser Reflection API
4
 *
5
 * @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
6
 *
7
 * This source file is subject to the license that is bundled
8
 * with this source code in the file LICENSE.
9
 */
10
11
namespace Go\ParserReflection;
12
13
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
14
use Go\ParserReflection\ValueResolver\NodeExpressionResolver;
15
use PhpParser\Node\Name;
16
use PhpParser\Node\Param;
17
use ReflectionParameter as BaseReflectionParameter;
18
19
/**
20
 * AST-based reflection for method/function parameter
21
 */
22
class ReflectionParameter extends BaseReflectionParameter
23
{
24
    use InternalPropertiesEmulationTrait;
25
26
    /**
27
     * Reflection function or method
28
     *
29
     * @var \ReflectionFunctionAbstract
30
     */
31
    private $declaringFunction;
32
33
    /**
34
     * Stores the default value for node (if present)
35
     *
36
     * @var mixed
37
     */
38
    private $defaultValue = null;
39
40
    /**
41
     * Whether or not default value is constant
42
     *
43
     * @var bool
44
     */
45
    private $isDefaultValueConstant = false;
46
47
    /**
48
     * Name of the constant of default value
49
     *
50
     * @var string
51
     */
52
    private $defaultValueConstantName;
53
54
    /**
55
     * Index of parameter in the list
56
     *
57
     * @var int
58
     */
59
    private $parameterIndex = 0;
60
61
    /**
62
     * Concrete parameter node
63
     *
64
     * @var Param
65
     */
66
    private $parameterNode;
67
68
    /**
69
     * Initializes a reflection for the property
70
     *
71
     * @param string|array $unusedFunctionName Name of the function/method
72
     * @param string $parameterName Name of the parameter to reflect
73
     * @param Param $parameterNode Parameter definition node
74
     * @param int $parameterIndex Index of parameter
75
     * @param \ReflectionFunctionAbstract $declaringFunction
76
     */
77 24
    public function __construct(
78
        $unusedFunctionName,
79
        $parameterName,
80
        Param $parameterNode = null,
81
        $parameterIndex = 0,
82
        \ReflectionFunctionAbstract $declaringFunction = null
83
    ) {
84
        // Let's unset original read-only property to have a control over it via __get
85 24
        unset($this->name);
86
87 24
        $this->parameterNode     = $parameterNode;
88 24
        $this->parameterIndex    = $parameterIndex;
89 24
        $this->declaringFunction = $declaringFunction;
90
91 24
        if ($this->isDefaultValueAvailable()) {
92 18
            if ($declaringFunction instanceof \ReflectionMethod) {
93 8
                $context = $declaringFunction->getDeclaringClass();
94
            } else {
95 10
                $context = $declaringFunction;
96
            };
97
98 18
            $expressionSolver = new NodeExpressionResolver($context);
99 18
            $expressionSolver->process($this->parameterNode->default);
100 18
            $this->defaultValue             = $expressionSolver->getValue();
101 18
            $this->isDefaultValueConstant   = $expressionSolver->isConstant();
102 18
            $this->defaultValueConstantName = $expressionSolver->getConstantName();
103
        }
104 24
    }
105
106
    /**
107
     * Emulating original behaviour of reflection
108
     */
109 1
    public function ___debugInfo()
110
    {
111
        return array(
112 1
            'name' => $this->parameterNode->name,
113
        );
114
    }
115
116
    /**
117
     * Returns string representation of this parameter.
118
     *
119
     * @return string
120
     */
121 13
    public function __toString()
122
    {
123 13
        $parameterType   = $this->getType();
124 13
        $isNullableParam = !empty($parameterType) && $this->allowsNull();
125 13
        $isOptional      = $this->isOptional();
126 13
        $hasDefaultValue = $this->isDefaultValueAvailable();
127 13
        $defaultValue    = '';
128 13
        if ($hasDefaultValue) {
129 8
            $defaultValue = $this->getDefaultValue();
130 8
            if (is_string($defaultValue) && strlen($defaultValue) > 15) {
131 3
                $defaultValue = substr($defaultValue, 0, 15) . '...';
132
            }
133
            /* @see https://3v4l.org/DJOEb for behaviour changes */
134 8
            if (is_double($defaultValue) && fmod($defaultValue, 1.0) === 0.0) {
135 3
                $defaultValue = (int) $defaultValue;
136
            }
137
138 8
            $defaultValue = str_replace('\\\\', '\\', var_export($defaultValue, true));
139
        }
140
141 13
        return sprintf(
142 13
            'Parameter #%d [ %s %s%s%s%s$%s%s ]',
143 13
            $this->parameterIndex,
144 13
            $isOptional ? '<optional>' : '<required>',
145 13
            $parameterType ? ReflectionType::convertToDisplayType($parameterType) . ' ' : '',
146 13
            $isNullableParam ? 'or NULL ' : '',
147 13
            $this->isVariadic() ? '...' : '',
148 13
            $this->isPassedByReference() ? '&' : '',
149 13
            $this->getName(),
150 13
            ($isOptional && $hasDefaultValue) ? (' = ' . $defaultValue) : ''
151
        );
152
    }
153
154
    /**
155
     * {@inheritDoc}
156
     */
157 14
    public function allowsNull()
158
    {
159 14
        $hasDefaultNull = $this->isDefaultValueAvailable() && $this->getDefaultValue() === null;
160 14
        if ($hasDefaultNull) {
161 3
            return true;
162
        }
163
164 13
        return !isset($this->parameterNode->type);
165
    }
166
167
    /**
168
     * {@inheritDoc}
169
     */
170 3
    public function canBePassedByValue()
171
    {
172 3
        return !$this->isPassedByReference();
173
    }
174
175
    /**
176
     * @inheritDoc
177
     */
178 2
    public function getClass()
179
    {
180 2
        $parameterType = $this->parameterNode->type;
181 2
        if ($parameterType instanceof Name) {
182 2
            if (!$parameterType instanceof Name\FullyQualified) {
183 1
                $parameterTypeName = $parameterType->toString();
184
185 1
                if ('self' === $parameterTypeName) {
186 1
                    return $this->getDeclaringClass();
187
                }
188
189 1
                if ('parent' === $parameterTypeName) {
190 1
                    return $this->getDeclaringClass()->getParentClass();
191
                }
192
193
                throw new ReflectionException("Can not resolve a class name for parameter");
194
            }
195 1
            $className   = $parameterType->toString();
196 1
            $classOrInterfaceExists = class_exists($className, false) || interface_exists($className, false);
197
198 1
            return $classOrInterfaceExists ? new \ReflectionClass($className) : new ReflectionClass($className);
199
        }
200
201 1
        return null;
202
    }
203
204
    /**
205
     * {@inheritDoc}
206
     */
207 3
    public function getDeclaringClass()
208
    {
209 3
        if ($this->declaringFunction instanceof \ReflectionMethod) {
210 2
            return $this->declaringFunction->getDeclaringClass();
211
        };
212
213 1
        return null;
214
    }
215
216
    /**
217
     * {@inheritDoc}
218
     */
219 12
    public function getDeclaringFunction()
220
    {
221 12
        return $this->declaringFunction;
222
    }
223
224
    /**
225
     * {@inheritDoc}
226
     */
227 11
    public function getDefaultValue()
228
    {
229 11
        if (!$this->isDefaultValueAvailable()) {
230 1
            throw new ReflectionException('Internal error: Failed to retrieve the default value');
231
        }
232
233 10
        return $this->defaultValue;
234
    }
235
236
    /**
237
     * {@inheritDoc}
238
     */
239 6
    public function getDefaultValueConstantName()
240
    {
241 6
        if (!$this->isDefaultValueAvailable()) {
242 1
            throw new ReflectionException('Internal error: Failed to retrieve the default value');
243
        }
244
245 5
        return $this->defaultValueConstantName;
246
    }
247
248
    /**
249
     * @inheritDoc
250
     */
251 14
    public function getName()
252
    {
253 14
        return $this->parameterNode->name;
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     */
259 3
    public function getPosition()
260
    {
261 3
        return $this->parameterIndex;
262
    }
263
264
    /**
265
     * @inheritDoc
266
     */
267 14
    public function getType()
268
    {
269 14
        $isBuiltin     = false;
270 14
        $allowsNull    = $this->allowsNull();
271 14
        $parameterType = $this->parameterNode->type;
272 14
        if (is_object($parameterType)) {
273 3
            $parameterType = $parameterType->toString();
0 ignored issues
show
The method toString does only exist in PhpParser\Node\Name, but not in PhpParser\Node\NullableType.

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...
274 14
        } elseif (is_string($parameterType)) {
275 10
            $isBuiltin = true;
276
        } else {
277 6
            return null;
278
        }
279
280 10
        return new ReflectionType($parameterType, $allowsNull, $isBuiltin);
281
    }
282
283
    /**
284
     * @inheritDoc
285
     */
286 4
    public function hasType()
287
    {
288 4
        $hasType = isset($this->parameterNode->type);
289
290 4
        return $hasType;
291
    }
292
293
    /**
294
     * @inheritDoc
295
     */
296 3
    public function isArray()
297
    {
298 3
        return 'array' === $this->parameterNode->type;
299
    }
300
301
    /**
302
     * @inheritDoc
303
     */
304 3
    public function isCallable()
305
    {
306 3
        return 'callable' === $this->parameterNode->type;
307
    }
308
309
    /**
310
     * @inheritDoc
311
     */
312 32
    public function isDefaultValueAvailable()
313
    {
314 32
        return isset($this->parameterNode->default);
315
    }
316
317
    /**
318
     * {@inheritDoc}
319
     */
320 5
    public function isDefaultValueConstant()
321
    {
322 5
        return $this->isDefaultValueConstant;
323
    }
324
325
    /**
326
     * {@inheritDoc}
327
     */
328 22
    public function isOptional()
329
    {
330 22
        return $this->isVariadic() || ($this->isDefaultValueAvailable() && $this->haveSiblingsDefalutValues());
331
    }
332
333
    /**
334
     * @inheritDoc
335
     */
336 13
    public function isPassedByReference()
337
    {
338 13
        return (bool) $this->parameterNode->byRef;
339
    }
340
341
    /**
342
     * @inheritDoc
343
     */
344 31
    public function isVariadic()
345
    {
346 31
        return (bool) $this->parameterNode->variadic;
347
    }
348
349
    /**
350
     * Returns if all following parameters have a default value definition.
351
     *
352
     * @return bool
353
     * @throws ReflectionException If could not fetch declaring function reflection
354
     */
355 12
    protected function haveSiblingsDefalutValues()
356
    {
357 12
        $function = $this->getDeclaringFunction();
358 12
        if (null === $function) {
359
            throw new ReflectionException('Could not get the declaring function reflection.');
360
        }
361
362
        /** @var \ReflectionParameter[] $remainingParameters */
363 12
        $remainingParameters = array_slice($function->getParameters(), $this->parameterIndex + 1);
364 12
        foreach ($remainingParameters as $reflectionParameter) {
365 12
            if (!$reflectionParameter->isDefaultValueAvailable()) {
366 12
                return false;
367
            }
368
        }
369
370 11
        return true;
371
    }
372
}
373