• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / Arrayy / 38037101317

10 Oct 2026 08:12AM UTC coverage: 92.728% (+0.3%) from 92.38%
38037101317

push

github

web-flow
Merge pull request #183 from voku/claude/finish-mutation-testing-yw8jy8

Enforce PHPStan and timeout-aware Infection mutation quality gates

21 of 21 new or added lines in 4 files covered. (100.0%)

6 existing lines in 2 files now uncovered.

2780 of 2998 relevant lines covered (92.73%)

250.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.29
/src/TypeCheck/TypeCheckPhpDoc.php
1
<?php
2

3
/** @noinspection TransitiveDependenciesUsageInspection */
4
/** @noinspection ClassReImplementsParentInterfaceInspection */
5

6
declare(strict_types=1);
7

8
namespace Arrayy\TypeCheck;
9

10
/**
11
 * inspired by https://github.com/spatie/value-object
12
 *
13
 * @internal
14
 */
15
final class TypeCheckPhpDoc extends AbstractTypeCheck implements TypeCheckInterface
16
{
17
    /**
18
     * @var string
19
     */
20
    private $property_name;
21

22
    /**
23
     * @param string $reflectionPropertyName
24
     */
25
    public function __construct($reflectionPropertyName)
26
    {
27
        $this->property_name = $reflectionPropertyName;
187 ✔
28
    }
29

30
    /**
31
     * @param \phpDocumentor\Reflection\DocBlock\Tags\Property $phpDocumentorReflectionProperty
32
     * @param string                                           $property
33
     *
34
     * @return self|null
35
     */
36
    public static function fromPhpDocumentorProperty(\phpDocumentor\Reflection\DocBlock\Tags\Property $phpDocumentorReflectionProperty, string $property = '')
37
    {
38
        if (!$property) {
56 ✔
39
            /** @var string|null $propertyTmp */
40
            $propertyTmp = $phpDocumentorReflectionProperty->getVariableName();
14 ✔
41
            if ($propertyTmp === null) {
14 ✔
42
                return null;
×
43
            }
44

45
            $property = $propertyTmp;
14 ✔
46
        }
47

48
        return self::fromDocTypeObject($property, $phpDocumentorReflectionProperty->getType());
56 ✔
49
    }
50

51
    /**
52
     * Create a type checker from a phpDocumentor type object and an explicit property name.
53
     *
54
     * @param string                              $property
55
     * @param \phpDocumentor\Reflection\Type|null $type
56
     *
57
     * @phpstan-param \phpDocumentor\Reflection\Type|null $type
58
     *
59
     * @return self
60
     */
61
    public static function fromDocTypeObject(string $property, $type)
62
    {
63
        $tmpReflection = new self($property);
140 ✔
64

65
        if ($type) {
140 ✔
66
            $docTypes = self::parseDocTypeObject($type);
133 ✔
67
            if (\is_array($docTypes) === true) {
133 ✔
68
                foreach ($docTypes as $docType) {
42 ✔
69
                    $tmpReflection->types[] = $docType;
42 ✔
70
                }
71
            } else {
72
                $tmpReflection->types[] = $docTypes;
126 ✔
73
            }
74

75
            if (\in_array('null', $tmpReflection->types, true)) {
133 ✔
76
                $tmpReflection->isNullable = true;
49 ✔
77
            }
78
        }
79

80
        return $tmpReflection;
140 ✔
81
    }
82

83
    public static function fromReflectionProperty(\ReflectionProperty $reflectionProperty): self
84
    {
85
        $tmpReflection = new self($reflectionProperty->getName());
47 ✔
86
        $type = $reflectionProperty->getType();
47 ✔
87
        $docTypes = self::getTypesFromReflectionPropertyDocBlock($reflectionProperty);
47 ✔
88

89
        if ($docTypes !== null) {
47 ✔
90
            if (\is_array($docTypes) === true) {
33 ✔
91
                foreach ($docTypes as $docType) {
7 ✔
92
                    $tmpReflection->types[] = $docType;
7 ✔
93
                }
94
            } else {
95
                $tmpReflection->types[] = $docTypes;
29 ✔
96
            }
97
        } elseif ($type === null) {
28 ✔
98
            $tmpReflection->types[] = 'mixed';
7 ✔
99
            $tmpReflection->isNullable = true;
7 ✔
100

101
            return $tmpReflection;
7 ✔
102
        } else {
103
            $docTypes = self::parseReflectionTypeObject($type);
21 ✔
104
            if (\is_array($docTypes) === true) {
21 ✔
105
                foreach ($docTypes as $docType) {
14 ✔
106
                    $tmpReflection->types[] = $docType;
14 ✔
107
                }
108
            } else {
109
                $tmpReflection->types[] = $docTypes;
21 ✔
110
            }
111
        }
112

113
        if ($type !== null && self::typeAllowsNull($type) && \in_array('null', $tmpReflection->types, true) === false) {
40 ✔
114
            $tmpReflection->types[] = 'null';
21 ✔
115
        }
116

117
        if (\in_array('null', $tmpReflection->types, true)) {
40 ✔
118
            $tmpReflection->isNullable = true;
21 ✔
119
        }
120

121
        return $tmpReflection;
40 ✔
122
    }
123

124
    /**
125
     * @param \phpDocumentor\Reflection\Type $type
126
     *
127
     * @return string|string[]
128
     */
129
    public static function parseDocTypeObject($type)
130
    {
131
        if ($type instanceof \phpDocumentor\Reflection\Types\Object_) {
166 ✔
132
            $tmpObject = (string) $type->getFqsen();
56 ✔
133
            if ($tmpObject) {
56 ✔
134
                return $tmpObject;
56 ✔
135
            }
136

137
            return 'object';
×
138
        }
139

140
        if ($type instanceof \phpDocumentor\Reflection\Types\Compound) {
152 ✔
141
            $types = [];
42 ✔
142
            foreach ($type as $subType) {
42 ✔
143
                $typeTmp = self::parseDocTypeObject($subType);
42 ✔
144

145
                /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
146
                /** @var string $typeTmp */
147
                $typeTmp = $typeTmp;
42 ✔
148

149
                $types[] = $typeTmp;
42 ✔
150
            }
151

152
            return $types;
42 ✔
153
        }
154

155
        if ($type instanceof \phpDocumentor\Reflection\Types\Nullable) {
152 ✔
156
            $typeTmp = self::parseDocTypeObject($type->getActualType());
7 ✔
157
            if (\is_array($typeTmp) === true) {
7 ✔
158
                $typeTmp[] = 'null';
×
159

160
                return $typeTmp;
×
161
            }
162

163
            return [$typeTmp, 'null'];
7 ✔
164
        }
165

166
        if (
167
            \class_exists('\phpDocumentor\Reflection\PseudoTypes\ArrayShape')
145 ✔
168
            &&
169
            $type instanceof \phpDocumentor\Reflection\PseudoTypes\ArrayShape
145 ✔
170
        ) {
171
            return 'array';
7 ✔
172
        }
173

174
        if ($type instanceof \phpDocumentor\Reflection\Types\Array_) {
138 ✔
175
            $valueTypeTmp = $type->getValueType()->__toString();
49 ✔
176
            if ($valueTypeTmp !== 'mixed') {
49 ✔
177
                return $valueTypeTmp . '[]';
49 ✔
178
            }
179

180
            return 'array';
×
181
        }
182

183
        if ($type instanceof \phpDocumentor\Reflection\Types\Null_) {
124 ✔
184
            return 'null';
42 ✔
185
        }
186

187
        if ($type instanceof \phpDocumentor\Reflection\Types\Mixed_) {
124 ✔
188
            return 'mixed';
7 ✔
189
        }
190

191
        foreach (self::getScalarPseudoTypeClasses() as $scalarTypeClass) {
124 ✔
192
            if ($type instanceof $scalarTypeClass) {
124 ✔
193
                return 'string|int|float|bool';
21 ✔
194
            }
195
        }
196

197
        if ($type instanceof \phpDocumentor\Reflection\Types\Boolean) {
117 ✔
198
            return 'bool';
7 ✔
199
        }
200

201
        if ($type instanceof \phpDocumentor\Reflection\Types\Callable_) {
117 ✔
202
            return 'callable';
7 ✔
203
        }
204

205
        if ($type instanceof \phpDocumentor\Reflection\Types\Float_) {
110 ✔
206
            return 'float';
7 ✔
207
        }
208

209
        if ($type instanceof \phpDocumentor\Reflection\Types\String_) {
110 ✔
210
            return 'string';
63 ✔
211
        }
212

213
        if ($type instanceof \phpDocumentor\Reflection\Types\Integer) {
82 ✔
214
            return 'int';
70 ✔
215
        }
216

217
        if ($type instanceof \phpDocumentor\Reflection\Types\Void_) {
12 ✔
218
            return 'void';
×
219
        }
220

221
        if ($type instanceof \phpDocumentor\Reflection\Types\Resource_) {
12 ✔
222
            return 'resource';
7 ✔
223
        }
224

UNCOV
225
        return $type->__toString();
5 ✔
226
    }
227

228
    /**
229
     * @return list<string>
230
     */
231
    private static function getScalarPseudoTypeClasses(): array
232
    {
233
        $classes = [];
124 ✔
234

235
        foreach (
236
            [
124 ✔
237
                '\phpDocumentor\Reflection\PseudoTypes\Scalar',
124 ✔
238
                '\phpDocumentor\Reflection\Types\Scalar',
124 ✔
239
            ] as $className
124 ✔
240
        ) {
241
            if (\class_exists($className)) {
124 ✔
242
                $classes[] = $className;
124 ✔
243
            }
244
        }
245

246
        return $classes;
124 ✔
247
    }
248

249
    /**
250
     * @return string|string[]|null
251
     */
252
    private static function getTypesFromReflectionPropertyDocBlock(\ReflectionProperty $reflectionProperty)
253
    {
254
        $docComment = $reflectionProperty->getDocComment();
47 ✔
255
        if ($docComment === false) {
47 ✔
256
            return null;
28 ✔
257
        }
258

259
        /** @var \phpDocumentor\Reflection\DocBlockFactoryInterface|null $factory cache factory to avoid recreating it per reflected property */
260
        static $factory = null;
33 ✔
261
        if ($factory === null) {
33 ✔
262
            $factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
7 ✔
263
        }
264
        $docblock = $factory->create($docComment);
33 ✔
265
        foreach ($docblock->getTagsByName('var') as $tag) {
33 ✔
266
            if (!$tag instanceof \phpDocumentor\Reflection\DocBlock\Tags\Var_) {
33 ✔
267
                continue;
×
268
            }
269

270
            /** @var \phpDocumentor\Reflection\Type|null $type */
271
            $type = $tag->getType();
33 ✔
272
            if ($type === null) {
33 ✔
273
                continue;
×
274
            }
275

276
            return self::parseDocTypeObject($type);
33 ✔
277
        }
278

279
        return null;
×
280
    }
281

282
    private static function typeAllowsNull(\ReflectionType $type): bool
283
    {
284
        if (
285
            \class_exists(\ReflectionIntersectionType::class)
33 ✔
286
            &&
287
            $type instanceof \ReflectionIntersectionType
33 ✔
288
        ) {
UNCOV
289
            return false;
5 ✔
290
        }
291

292
        return $type->allowsNull();
28 ✔
293
    }
294

295
    /**
296
     * @param \ReflectionType $type
297
     *
298
     * @return string|string[]
299
     */
300
    public static function parseReflectionTypeObject(\ReflectionType $type)
301
    {
302
        if ($type instanceof \ReflectionNamedType) {
21 ✔
303
            $typeName = $type->getName();
21 ✔
304

305
            if ($type->isBuiltin()) {
21 ✔
306
                return $typeName;
21 ✔
307
            }
308

309
            return '\\' . \ltrim($typeName, '\\');
14 ✔
310
        }
311

312
        if ($type instanceof \ReflectionUnionType) {
14 ✔
313
            $types = [];
14 ✔
314
            foreach ($type->getTypes() as $subType) {
14 ✔
315
                $typeTmp = self::parseReflectionTypeObject($subType);
14 ✔
316
                if (\is_array($typeTmp)) {
14 ✔
317
                    foreach ($typeTmp as $typeTmpInner) {
×
318
                        $types[] = $typeTmpInner;
×
319
                    }
320
                } else {
321
                    $types[] = $typeTmp;
14 ✔
322
                }
323
            }
324

325
            return $types;
14 ✔
326
        }
327

328
        if (
329
            \class_exists(\ReflectionIntersectionType::class)
×
330
            &&
331
            $type instanceof \ReflectionIntersectionType
×
332
        ) {
333
            $types = [];
×
334
            foreach ($type->getTypes() as $subType) {
×
335
                $typeTmp = self::parseReflectionTypeObject($subType);
×
336
                if (\is_array($typeTmp)) {
×
337
                    foreach ($typeTmp as $typeTmpInner) {
×
338
                        $types[] = $typeTmpInner;
×
339
                    }
340
                } else {
341
                    $types[] = $typeTmp;
×
342
                }
343
            }
344

345
            return \implode('&', $types);
×
346
        }
347

348
        return (string) $type;
×
349
    }
350

351
    /**
352
     * @param string $expectedTypes
353
     * @param mixed  $value
354
     * @param string $type
355
     *
356
     * @return \TypeError
357
     */
358
    public function throwException($expectedTypes, $value, $type): \Throwable
359
    {
360
        throw new \TypeError("Invalid type: expected \"{$this->property_name}\" to be of type {{$expectedTypes}}, instead got value \"" . $this->valueToString($value) . '" (' . \print_r($value, true) . ") with type {{$type}}.");
196 ✔
361
    }
362
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc