Completed
Pull Request — master (#1289)
by Johannes
13:58
created

DocBlockTypeResolver   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 269
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 90
c 3
b 0
f 0
dl 0
loc 269
rs 8.96
wmc 43

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A resolveType() 0 7 3
B getPropertyDocblockTypeHint() 0 59 8
A resolveTypeFromTypeNode() 0 7 2
A getDeclaringClassOrTrait() 0 11 4
A isSimpleType() 0 3 2
A hasGlobalNamespacePrefix() 0 3 1
A flattenVarTagValueTypes() 0 9 2
A gatherSingleUseStatements() 0 9 2
A isNullType() 0 3 1
A gatherGroupUseStatements() 0 11 3
A isPrimitiveType() 0 3 1
B expandClassNameUsingUseStatements() 0 28 8
A endsWith() 0 5 1
A isClassOrInterface() 0 3 2
A filterNullFromTypes() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like DocBlockTypeResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocBlockTypeResolver, and based on these observations, apply Extract Interface, too.

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
     *
52
     * @return string|null
53
     */
54
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
55
    {
56
        if (!$reflectionProperty->getDocComment()) {
57
            return null;
58
        }
59
60
        // First we tokenize the PhpDoc comment and parse the tokens into a PhpDocNode.
61
        $tokens = $this->lexer->tokenize($reflectionProperty->getDocComment());
62
        $phpDocNode = $this->phpDocParser->parse(new TokenIterator($tokens));
63
64
        // Then we retrieve a flattened list of annotated types excluding null.
65
        $varTagValues = $phpDocNode->getVarTagValues();
66
        $types = $this->flattenVarTagValueTypes($varTagValues);
67
        $typesWithoutNull = $this->filterNullFromTypes($types);
68
69
        // The PhpDoc does not contain additional type information.
70
        if (0 === count($typesWithoutNull)) {
71
            return null;
72
        }
73
74
        // The PhpDoc contains multiple non-null types which produces ambiguity when deserializing.
75
        if (count($typesWithoutNull) > 1) {
76
            $typeHint = implode('|', array_map(static function (TypeNode $type) {
77
                return (string) $type;
78
            }, $types));
79
80
            throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
81
        }
82
83
        // Only one type is left, so we only need to differentiate between arrays, generics and other types.
84
        $type = $typesWithoutNull[0];
85
86
        // Simple array without concrete type: array
87
        if ($this->isSimpleType($type, 'array')) {
88
            return null;
89
        }
90
91
        // Normal array syntax: Product[] | \Foo\Bar\Product[]
92
        if ($type instanceof ArrayTypeNode) {
93
            $resolvedType = $this->resolveTypeFromTypeNode($type->type, $reflectionProperty);
94
95
            return 'array<' . $resolvedType . '>';
96
        }
97
98
        // Generic array syntax: array<Product> | array<\Foo\Bar\Product> | array<int,Product>
99
        if ($type instanceof GenericTypeNode) {
100
            if (!$this->isSimpleType($type->type, 'array')) {
101
                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()));
102
            }
103
104
            $resolvedTypes = array_map(function (TypeNode $node) use ($reflectionProperty) {
105
                return $this->resolveTypeFromTypeNode($node, $reflectionProperty);
106
            }, $type->genericTypes);
107
108
            return 'array<' . implode(',', $resolvedTypes) . '>';
109
        }
110
111
        // Primitives and class names: Collection | \Foo\Bar\Product | string
112
        return $this->resolveTypeFromTypeNode($type, $reflectionProperty);
113
    }
114
115
    /**
116
     * Returns a flat list of types of the given var tags. Union types are flattened as well.
117
     *
118
     * @param VarTagValueNode[] $varTagValues
119
     *
120
     * @return TypeNode[]
121
     */
122
    private function flattenVarTagValueTypes(array $varTagValues): array
123
    {
124
        return array_merge(...array_map(static function (VarTagValueNode $node) {
125
            if ($node->type instanceof UnionTypeNode) {
126
                return $node->type->types;
127
            }
128
129
            return [$node->type];
130
        }, $varTagValues));
131
    }
132
133
    /**
134
     * Filters the null type from the given types array. If no null type is found, the array is returned unchanged.
135
     *
136
     * @param TypeNode[] $types
137
     *
138
     * @return TypeNode[]
139
     */
140
    private function filterNullFromTypes(array $types): array
141
    {
142
        return array_filter(array_map(function (TypeNode $node) {
143
            return $this->isNullType($node) ? null : $node;
144
        }, $types));
145
    }
146
147
    /**
148
     * Determines if the given type is a null type.
149
     *
150
     * @param TypeNode $typeNode
151
     *
152
     * @return bool
153
     */
154
    private function isNullType(TypeNode $typeNode): bool
155
    {
156
        return $this->isSimpleType($typeNode, 'null');
157
    }
158
159
    /**
160
     * Determines if the given node represents a simple type.
161
     *
162
     * @param TypeNode $typeNode
163
     * @param string $simpleType
164
     *
165
     * @return bool
166
     */
167
    private function isSimpleType(TypeNode $typeNode, string $simpleType): bool
168
    {
169
        return $typeNode instanceof IdentifierTypeNode && $typeNode->name === $simpleType;
170
    }
171
172
    /**
173
     * Attempts to resolve the fully qualified type from the given node. If the node is not suitable for type
174
     * retrieval, an exception is thrown.
175
     *
176
     * @param TypeNode $typeNode
177
     * @param \ReflectionProperty $reflectionProperty
178
     *
179
     * @return string
180
     *
181
     * @throws \InvalidArgumentException
182
     */
183
    private function resolveTypeFromTypeNode(TypeNode $typeNode, \ReflectionProperty $reflectionProperty): string
184
    {
185
        if (!($typeNode instanceof IdentifierTypeNode)) {
186
            throw new \InvalidArgumentException(sprintf("Can't use unsupported type %s for collection in %s:%s", (string) $typeNode, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
187
        }
188
189
        return $this->resolveType($typeNode->name, $reflectionProperty);
190
    }
191
192
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
193
    {
194
        if ($this->isClassOrInterface($typeHint)) {
195
            return $typeHint;
196
        }
197
198
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
199
        if ($this->isClassOrInterface($expandedClassName)) {
200
            return $expandedClassName;
201
        }
202
203
        $classContents = file_get_contents($declaringClass->getFileName());
204
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
205
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
206
207
        foreach ($foundUseStatements as $statementClassName) {
208
            if ($alias = explode('as', $statementClassName)) {
209
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
210
                    return trim($alias[0]);
211
                }
212
            }
213
214
            if ($this->endsWith($statementClassName, $typeHint)) {
215
                return $statementClassName;
216
            }
217
        }
218
219
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
220
    }
221
222
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
223
    {
224
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
225
226
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
227
    }
228
229
    private function isPrimitiveType(string $type): bool
230
    {
231
        return in_array($type, ['int', 'float', 'bool', 'string']);
232
    }
233
234
    private function hasGlobalNamespacePrefix(string $typeHint): bool
235
    {
236
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
237
    }
238
239
    private function gatherGroupUseStatements(string $classContents): array
240
    {
241
        $foundUseStatements = [];
242
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
243
        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...
244
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
245
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
246
            }
247
        }
248
249
        return $foundUseStatements;
250
    }
251
252
    private function gatherSingleUseStatements(string $classContents): array
253
    {
254
        $foundUseStatements = [];
255
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
256
        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...
257
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
258
        }
259
260
        return $foundUseStatements;
261
    }
262
263
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
264
    {
265
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
266
            foreach ($trait->getProperties() as $traitProperty) {
267
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
268
                    return $this->getDeclaringClassOrTrait($traitProperty);
269
                }
270
            }
271
        }
272
273
        return $reflectionProperty->getDeclaringClass();
274
    }
275
276
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
277
    {
278
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
279
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
280
        }
281
282
        return ltrim($typeHint, '\\');
283
    }
284
285
    private function isClassOrInterface(string $typeHint): bool
286
    {
287
        return class_exists($typeHint) || interface_exists($typeHint);
288
    }
289
}
290