Completed
Pull Request — master (#1261)
by
unknown
11:09
created

DocBlockTypeResolver::isNullType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\Metadata\Driver;
6
7
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
8
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
9
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
10
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
11
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
12
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
13
use PHPStan\PhpDocParser\Lexer\Lexer;
14
use PHPStan\PhpDocParser\Parser\ConstExprParser;
15
use PHPStan\PhpDocParser\Parser\PhpDocParser;
16
use PHPStan\PhpDocParser\Parser\TokenIterator;
17
use PHPStan\PhpDocParser\Parser\TypeParser;
18
19
class DocBlockTypeResolver
20
{
21
    /** resolve single use statements */
22
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
23
    /** resolve group use statements */
24
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
25
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
26
27
    /**
28
     * @var PhpDocParser
29
     */
30
    protected $phpDocParser;
31
32
    /**
33
     * @var Lexer
34
     */
35
    protected $lexer;
36
37
    public function __construct()
38
    {
39
        $constExprParser = new ConstExprParser();
40
        $typeParser = new TypeParser($constExprParser);
41
42
        $this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
43
        $this->lexer = new Lexer();
44
    }
45
46
    /**
47
     * Attempts to retrieve additional type information from a PhpDoc block. Throws in case of ambiguous type
48
     * information and will return null if no helpful type information could be retrieved.
49
     *
50
     * @param \ReflectionProperty $reflectionProperty
51
     * @return string|null
52
     */
53
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
54
    {
55
        if (!$reflectionProperty->getDocComment()) {
56
            return null;
57
        }
58
59
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
60
        $tokens = $this->lexer->tokenize($reflectionProperty->getDocComment());
0 ignored issues
show
Bug introduced by
It seems like $reflectionProperty->getDocComment() can also be of type true; however, parameter $s of PHPStan\PhpDocParser\Lexer\Lexer::tokenize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

60
        $tokens = $this->lexer->tokenize(/** @scrutinizer ignore-type */ $reflectionProperty->getDocComment());
Loading history...
61
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
62
63
        // Then we retrieve a flattened list of annotated types excluding null.
64
        $varTagValues = $phpDocNode->getVarTagValues();
65
        $types = $this->flattenVarTagValueTypes($varTagValues);
66
        $typesWithoutNull = $this->filterNullFromTypes($types);
67
68
        // The PhpDoc does not contain additional type information.
69
        if (count($typesWithoutNull) === 0) {
70
            return null;
71
        }
72
73
        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
74
        if (count($typesWithoutNull) > 1) {
75
            $typeHint = implode('|', array_map(function (TypeNode $type) {
76
                return (string) $type;
77
            }, $types));
78
            throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
79
        }
80
81
        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
82
        /** @var TypeNode $type */
83
        $type = $typesWithoutNull[0];
84
85
        // Simple array without concrete type: array
86
        if ($this->isSimpleType($type, 'array')) {
87
            return null;
88
        }
89
90
        // Normal array syntax: Product[] | \Foo\Bar\Product[]
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
91
        if ($type instanceof ArrayTypeNode) {
92
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflectionProperty);
93
94
            return 'array<' . $resolvedType . '>';
95
        }
96
97
        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
98
        if ($type instanceof GenericTypeNode) {
99
            if (!$this->isSimpleType($type->type, 'array')) {
100
                throw new \InvalidArgumentException(sprintf("Can't use non-array generic type %s for collection in %s:%s", (string) $type->type, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
101
            }
102
103
            $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) {
104
                return $this->resolveTypeFromTypeNode($node, $reflectionProperty);
105
            }, $type->genericTypes);
106
107
            return 'array<' . implode(',', $resolvedTypes) . '>';
108
        }
109
110
        // Primitives and class names: Collection | \Foo\Bar\Product | string
111
        return $this->resolveTypeFromTypeNode($type, $reflectionProperty);
112
    }
113
114
    /**
115
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
116
     *
117
     * @param VarTagValueNode[] $varTagValues
118
     * @return TypeNode[]
119
     */
120
    private function flattenVarTagValueTypes(array $varTagValues): array
121
    {
122
        return array_merge(...array_map(function (VarTagValueNode $node) {
123
            if ($node->type instanceof UnionTypeNode) {
124
                return $node->type->types;
125
            }
126
127
            return [$node->type];
128
        }, $varTagValues));
129
    }
130
131
    /**
132
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
133
     *
134
     * @param TypeNode[] $types
135
     * @return TypeNode[]
136
     */
137
    private function filterNullFromTypes(array $types): array
138
    {
139
        return array_filter(array_map(function (TypeNode $node) {
140
            return $this->isNullType($node) ? null : $node;
141
        }, $types));
142
    }
143
144
    /**
145
     * Determines if the given type is a null type.
146
     *
147
     * @param TypeNode $typeNode
148
     * @return boolean
149
     */
150
    private function isNullType(TypeNode $typeNode): bool
151
    {
152
        return $this->isSimpleType($typeNode, 'null');
153
    }
154
155
    /**
156
     * Determines if the given node represents a simple type.
157
     *
158
     * @param TypeNode $typeNode
159
     * @param string $simpleType
160
     * @return boolean
161
     */
162
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
163
    {
164
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
165
    }
166
167
    /**
168
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
169
     * retrieval, an exception is thrown.
170
     *
171
     * @param TypeNode $typeNode
172
     * @param \ReflectionProperty $reflectionProperty
173
     * @return string
174
     * @throws \InvalidArgumentException
175
     */
176
    private function resolveTypeFromTypeNode(TypeNode $typeNode, \ReflectionProperty $reflectionProperty): string
177
    {
178
        if (!($typeNode instanceof IdentifierTypeNode)) {
179
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
180
        }
181
182
        return $this->resolveType($typeNode->name, $reflectionProperty);
183
    }
184
185
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
186
    {
187
        if (class_exists($typeHint)) {
188
            return $typeHint;
189
        }
190
191
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
192
        if (class_exists($expandedClassName)) {
193
            return $expandedClassName;
194
        }
195
196
        $classContents = file_get_contents($declaringClass->getFileName());
197
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
198
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
199
200
        foreach ($foundUseStatements as $statementClassName) {
201
            if ($alias = explode('as', $statementClassName)) {
202
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
203
                    return trim($alias[0]);
204
                }
205
            }
206
207
            if ($this->endsWith($statementClassName, $typeHint)) {
208
                return $statementClassName;
209
            }
210
        }
211
212
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
213
    }
214
215
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
216
    {
217
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
218
219
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
220
    }
221
222
    private function isPrimitiveType(string $type): bool
223
    {
224
        return in_array($type, ['int', 'float', 'bool', 'string']);
225
    }
226
227
    private function hasGlobalNamespacePrefix(string $typeHint): bool
228
    {
229
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
230
    }
231
232
    private function gatherGroupUseStatements(string $classContents): array
233
    {
234
        $foundUseStatements = [];
235
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
236
        for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
237
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
238
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
239
            }
240
        }
241
242
        return $foundUseStatements;
243
    }
244
245
    private function gatherSingleUseStatements(string $classContents): array
246
    {
247
        $foundUseStatements = [];
248
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
249
        for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
250
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
251
        }
252
253
        return $foundUseStatements;
254
    }
255
256
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
257
    {
258
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
259
            foreach ($trait->getProperties() as $traitProperty) {
260
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
261
                    return $this->getDeclaringClassOrTrait($traitProperty);
262
                }
263
            }
264
        }
265
266
        return $reflectionProperty->getDeclaringClass();
267
    }
268
269
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
270
    {
271
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
272
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
273
        }
274
275
        return ltrim($typeHint, '\\');
276
    }
277
}
278