Completed
Pull Request — master (#1)
by David
04:52
created

AbstractMissingTypeHintRule::isInherited()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 11
nc 10
nop 2
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\PHPStan\Rules\TypeHints;
5
6
7
use BetterReflection\Reflection\ReflectionClass;
8
use BetterReflection\Reflection\ReflectionFunction;
9
use BetterReflection\Reflection\ReflectionMethod;
10
use BetterReflection\Reflection\ReflectionParameter;
11
use TheCodingMachine\PHPStan\BetterReflection\FindReflectionOnLine;
12
use phpDocumentor\Reflection\Type;
13
use phpDocumentor\Reflection\Types\Array_;
14
use phpDocumentor\Reflection\Types\Boolean;
15
use phpDocumentor\Reflection\Types\Callable_;
16
use phpDocumentor\Reflection\Types\Float_;
17
use phpDocumentor\Reflection\Types\Integer;
18
use phpDocumentor\Reflection\Types\Mixed;
19
use phpDocumentor\Reflection\Types\Null_;
20
use phpDocumentor\Reflection\Types\Object_;
21
use phpDocumentor\Reflection\Types\Scalar;
22
use phpDocumentor\Reflection\Types\String_;
23
use PhpParser\Node;
24
use PHPStan\Analyser\Scope;
25
use PHPStan\Broker\Broker;
26
use PHPStan\Rules\Rule;
27
28
abstract class AbstractMissingTypeHintRule implements Rule
29
{
30
31
    /**
32
     * @var Broker
33
     */
34
    private $broker;
35
36
    public function __construct(Broker $broker)
37
    {
38
39
        $this->broker = $broker;
40
    }
41
42
    abstract public function getNodeType(): string;
43
44
    /**
45
     * @param ReflectionMethod|ReflectionFunction $reflection
46
     * @return string
47
     */
48
    abstract public function getContext($reflection): string;
49
50
    /**
51
     * @param \PhpParser\Node\Stmt\Function_ $node
52
     * @param \PHPStan\Analyser\Scope $scope
53
     * @return string[]
54
     */
55
    public function processNode(Node $node, Scope $scope): array
56
    {
57
        // TODO: improve performance by caching better reflection results.
58
        $finder = FindReflectionOnLine::buildDefaultFinder();
59
60
        $reflection = $finder($scope->getFile(), $node->getLine());
61
62
        // If the method implements/extends another method, we have no choice on the signature so let's bypass this check.
63
        if ($reflection instanceof ReflectionMethod && $this->isInherited($reflection)) {
64
            return [];
65
        }
66
67
        $errors = [];
68
69
        foreach ($reflection->getParameters() as $parameter) {
70
            $result = $this->analyzeParameter($parameter);
71
72
            if ($result !== null) {
73
                $errors[] = $result;
74
            }
75
        }
76
77
        $returnTypeError = $this->analyzeReturnType($reflection);
78
        if ($returnTypeError !== null) {
79
            $errors[] = $returnTypeError;
80
        }
81
82
        return $errors;
83
    }
84
85
    /**
86
     * Analyzes a parameter and returns the error string if xomething goes wrong or null if everything is ok.
87
     *
88
     * @param ReflectionParameter $parameter
89
     * @return null|string
90
     */
91
    private function analyzeParameter(ReflectionParameter $parameter): ?string
92
    {
93
        $phpTypeHint = $parameter->getTypeHint();
94
        $docBlockTypeHints = $parameter->getDocBlockTypes();
95
96
        // If there is a type-hint, we have nothing to say unless it is an array.
97
        if ($phpTypeHint !== null) {
98
            return $this->analyzeWithTypehint($parameter, $phpTypeHint, $docBlockTypeHints);
99
        } else {
100
            return $this->analyzeWithoutTypehint($parameter, $docBlockTypeHints);
101
        }
102
    }
103
104
    /**
105
     * @param ReflectionFunction|ReflectionMethod $function
106
     * @return null|string
107
     */
108
    private function analyzeReturnType($function): ?string
109
    {
110
        $reflectionPhpTypeHint = $function->getReturnType();
111
        $phpTypeHint = null;
112
        if ($reflectionPhpTypeHint !== null) {
113
            $phpTypeHint = $reflectionPhpTypeHint->getTypeObject();
114
        }
115
        $docBlockTypeHints = $function->getDocBlockReturnTypes();
116
117
        // If there is a type-hint, we have nothing to say unless it is an array.
118
        if ($phpTypeHint !== null) {
119
            return $this->analyzeWithTypehint($function, $phpTypeHint, $docBlockTypeHints);
120
        } else {
121
            return $this->analyzeWithoutTypehint($function, $docBlockTypeHints);
122
        }
123
    }
124
125
    /**
126
     * @param ReflectionParameter|ReflectionMethod|ReflectionFunction $context
127
     * @param Type $phpTypeHint
128
     * @param Type[] $docBlockTypeHints
129
     * @return null|string
130
     */
131
    private function analyzeWithTypehint($context, Type $phpTypeHint, array $docBlockTypeHints): ?string
132
    {
133
        $docblockWithoutNullable = $this->typesWithoutNullable($docBlockTypeHints);
134
135
        if (!$phpTypeHint instanceof Array_) {
136
            // Let's detect mismatches between docblock and PHP typehint
137
            foreach ($docblockWithoutNullable as $docblockTypehint) {
138
                if (get_class($docblockTypehint) !== get_class($phpTypeHint)) {
139
                    if ($context instanceof ReflectionParameter) {
140
                        return sprintf('%s, parameter $%s type is type-hinted to "%s" but the @param annotation says it is a "%s". Please fix the @param annotation.', $this->getContext($context), $context->getName(), (string) $phpTypeHint, (string) $docblockTypehint);
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
141
                    } else {
142
                        return sprintf('%s, return type is type-hinted to "%s" but the @return annotation says it is a "%s". Please fix the @return annotation.', $this->getContext($context), (string) $phpTypeHint, (string) $docblockTypehint);
143
                    }
144
                }
145
            }
146
147
            return null;
148
        }
149
150
        if (empty($docblockWithoutNullable)) {
151
            if ($context instanceof ReflectionParameter) {
152
                return sprintf('%s, parameter $%s type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @param int[] $%s', $this->getContext($context), $context->getName(), $context->getName());
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
153
            } else {
154
                return sprintf('%s, return type is "array". Please provide a @param annotation to further specify the type of the array. For instance: @return int[]', $this->getContext($context));
155
            }
156
        } else {
157
            foreach ($docblockWithoutNullable as $docblockTypehint) {
158
                if (!$docblockTypehint instanceof Array_) {
159
                    if ($context instanceof ReflectionParameter) {
160
                        return sprintf('%s, mismatching type-hints for parameter %s. PHP type hint is "array" and docblock type hint is %s.', $this->getContext($context), $context->getName(), (string)$docblockTypehint);
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
161
                    } else {
162
                        return sprintf('%s, mismatching type-hints for return type. PHP type hint is "array" and docblock declared return type is %s.', $this->getContext($context), (string)$docblockTypehint);
163
                    }
164
                }
165
166
                if ($docblockTypehint->getValueType() instanceof Mixed) {
167
                    if ($context instanceof ReflectionParameter) {
168
                        return sprintf('%s, parameter $%s type is "array". Please provide a more specific @param annotation. For instance: @param int[] $%s', $this->getContext($context), $context->getName(), $context->getName());
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
169
                    } else {
170
                        return sprintf('%s, return type is "array". Please provide a more specific @return annotation. For instance: @return int[]', $this->getContext($context));
171
                    }
172
                }
173
            }
174
        }
175
176
        return null;
177
    }
178
179
    /**
180
     * @param ReflectionParameter|ReflectionMethod|ReflectionFunction $context
181
     * @param Type[] $docBlockTypeHints
182
     * @return null|string
183
     */
184
    private function analyzeWithoutTypehint($context, array $docBlockTypeHints): ?string
185
    {
186
        if (empty($docBlockTypeHints)) {
187
            if ($context instanceof ReflectionParameter) {
188
                return sprintf('%s, parameter $%s has no type-hint and no @param annotation.', $this->getContext($context), $context->getName());
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
189
            } else {
190
                return sprintf('%s, there is no return type and no @return annotation.', $this->getContext($context));
191
            }
192
        }
193
194
        $nativeTypehint = $this->isNativelyTypehintable($docBlockTypeHints);
195
196
        if ($nativeTypehint !== null) {
197
            if ($context instanceof ReflectionParameter) {
198
                return sprintf('%s, parameter $%s can be type-hinted to "%s".', $this->getContext($context), $context->getName(), $nativeTypehint);
0 ignored issues
show
Documentation introduced by
$context is of type object<BetterReflection\...on\ReflectionParameter>, but the function expects a object<BetterReflection\...ion\ReflectionFunction>.

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...
199
            } else {
200
                return sprintf('%s, a "%s" return type can be added.', $this->getContext($context), $nativeTypehint);
201
            }
202
        }
203
204
        return null;
205
    }
206
207
    /**
208
     * @param Type[] $docBlockTypeHints
209
     * @return string|null
210
     */
211
    private function isNativelyTypehintable(array $docBlockTypeHints): ?string
212
    {
213
        if (count($docBlockTypeHints) > 2) {
214
            return null;
215
        }
216
        $isNullable = $this->isNullable($docBlockTypeHints);
217
        if (count($docBlockTypeHints) === 2 && !$isNullable) {
218
            return null;
219
        }
220
221
        $types = $this->typesWithoutNullable($docBlockTypeHints);
222
        // At this point, there is at most one element here
223
        if (empty($types)) {
224
            return null;
225
        }
226
227
        $type = $types[0];
228
229
        if ($this->isNativeType($type)) {
230
            return ($isNullable?'?':'').((string)$type);
231
        }
232
233
        if ($type instanceof Array_) {
234
            return ($isNullable?'?':'').'array';
235
        }
236
237
        // TODO: more definitions to add here
238
        // Manage interface/classes
239
        // Manage array of things => (cast to array)
240
241
        if ($type instanceof Object_) {
242
            return ($isNullable?'?':'').((string)$type);
243
        }
244
245
        return null;
246
    }
247
248
    private function isNativeType(Type $type): bool
249
    {
250
        if ($type instanceof String_
251
            || $type instanceof Integer
252
            || $type instanceof Boolean
253
            || $type instanceof Float_
254
            || $type instanceof Scalar
255
            || $type instanceof Callable_
256
            || ((string) $type) === 'iterable'
257
        ) {
258
            return true;
259
        }
260
        return false;
261
    }
262
263
    /**
264
     * @param Type[] $docBlockTypeHints
265
     * @return bool
266
     */
267
    private function isNullable(array $docBlockTypeHints): bool
268
    {
269
        foreach ($docBlockTypeHints as $docBlockTypeHint) {
270
            if ($docBlockTypeHint instanceof Null_) {
271
                return true;
272
            }
273
        }
274
        return false;
275
    }
276
277
    /**
278
     * Removes "null" from the list of types.
279
     *
280
     * @param Type[] $docBlockTypeHints
281
     * @return array
282
     */
283
    private function typesWithoutNullable(array $docBlockTypeHints): array
284
    {
285
        return array_filter($docBlockTypeHints, function($item) {
286
            return !$item instanceof Null_;
287
        });
288
    }
289
290
    private function isInherited(ReflectionMethod $method, ReflectionClass $class = null): bool
291
    {
292
        if ($class === null) {
293
            $class = $method->getDeclaringClass();
294
        }
295
        $interfaces = $class->getInterfaces();
296
        foreach ($interfaces as $interface) {
297
            if ($interface->hasMethod($method->getName())) {
298
                return true;
299
            }
300
        }
301
302
        $parentClass = $class->getParentClass();
303
        if ($parentClass !== null) {
304
            return $this->isInherited($method, $parentClass);
305
        }
306
307
        return false;
308
    }
309
}
310