Test::getSize()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 2
dl 0
loc 17
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * This file is part of PHPUnit.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace PHPUnit\Util;
11
12
use PharIo\Version\VersionConstraintParser;
13
use PHPUnit\Framework\Assert;
14
use PHPUnit\Framework\CodeCoverageException;
15
use PHPUnit\Framework\Exception;
16
use PHPUnit\Framework\InvalidCoversTargetException;
17
use PHPUnit\Framework\SelfDescribing;
18
use PHPUnit\Framework\SkippedTestError;
19
use PHPUnit\Framework\TestCase;
20
use PHPUnit\Framework\Warning;
21
use PHPUnit\Runner\Version;
22
use ReflectionClass;
23
use ReflectionException;
24
use ReflectionFunction;
25
use ReflectionMethod;
26
use SebastianBergmann\Environment\OperatingSystem;
27
use Traversable;
28
29
final class Test
30
{
31
    /**
32
     * @var int
33
     */
34
    public const UNKNOWN = -1;
35
36
    /**
37
     * @var int
38
     */
39
    public const SMALL = 0;
40
41
    /**
42
     * @var int
43
     */
44
    public const MEDIUM = 1;
45
46
    /**
47
     * @var int
48
     */
49
    public const LARGE = 2;
50
51
    /**
52
     * @var string
53
     *
54
     * @todo This constant should be private (it's public because of TestTest::testGetProvidedDataRegEx)
55
     */
56
    public const REGEX_DATA_PROVIDER = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/';
57
58
    /**
59
     * @var string
60
     */
61
    private const REGEX_TEST_WITH = '/@testWith\s+/';
62
63
    /**
64
     * @var string
65
     */
66
    private const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';
67
68
    /**
69
     * @var string
70
     */
71
    private const REGEX_REQUIRES_VERSION = '/@requires\s+(?P<name>PHP(?:Unit)?)\s+(?P<operator>[<>=!]{0,2})\s*(?P<version>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m';
72
73
    /**
74
     * @var string
75
     */
76
    private const REGEX_REQUIRES_VERSION_CONSTRAINT = '/@requires\s+(?P<name>PHP(?:Unit)?)\s+(?P<constraint>[\d\t \-.|~^]+)[ \t]*\r?$/m';
77
78
    /**
79
     * @var string
80
     */
81
    private const REGEX_REQUIRES_OS = '/@requires\s+(?P<name>OS(?:FAMILY)?)\s+(?P<value>.+?)[ \t]*\r?$/m';
82
83
    /**
84
     * @var string
85
     */
86
    private const REGEX_REQUIRES_SETTING = '/@requires\s+(?P<name>setting)\s+(?P<setting>([^ ]+?))\s*(?P<value>[\w\.-]+[\w\.]?)?[ \t]*\r?$/m';
87
88
    /**
89
     * @var string
90
     */
91
    private const REGEX_REQUIRES = '/@requires\s+(?P<name>function|extension)\s+(?P<value>([^\s<>=!]+))\s*(?P<operator>[<>=!]{0,2})\s*(?P<version>[\d\.-]+[\d\.]?)?[ \t]*\r?$/m';
92
93
    /**
94
     * @var array
95
     */
96
    private static $annotationCache = [];
97
98
    /**
99
     * @var array
100
     */
101
    private static $hookMethods = [];
102
103
    public static function describe(\PHPUnit\Framework\Test $test): array
104
    {
105
        if ($test instanceof TestCase) {
106
            return [\get_class($test), $test->getName()];
107
        }
108
109
        if ($test instanceof SelfDescribing) {
110
            return ['', $test->toString()];
111
        }
112
113
        return ['', \get_class($test)];
114
    }
115
116
    public static function describeAsString(\PHPUnit\Framework\Test $test): string
117
    {
118
        if ($test instanceof SelfDescribing) {
119
            return $test->toString();
120
        }
121
122
        return \get_class($test);
123
    }
124
125
    /**
126
     * @throws CodeCoverageException
127
     *
128
     * @return array|bool
129
     */
130
    public static function getLinesToBeCovered(string $className, string $methodName)
131
    {
132
        $annotations = self::parseTestMethodAnnotations(
133
            $className,
134
            $methodName
135
        );
136
137
        if (self::shouldCoversAnnotationBeUsed($annotations) === false) {
138
            return false;
139
        }
140
141
        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
142
    }
143
144
    /**
145
     * Returns lines of code specified with the @uses annotation.
146
     *
147
     * @throws CodeCoverageException
148
     */
149
    public static function getLinesToBeUsed(string $className, string $methodName): array
150
    {
151
        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
152
    }
153
154
    /**
155
     * Returns the requirements for a test.
156
     *
157
     * @throws Warning
158
     */
159
    public static function getRequirements(string $className, string $methodName): array
160
    {
161
        $reflector  = new ReflectionClass($className);
162
        $docComment = $reflector->getDocComment();
163
        $reflector  = new ReflectionMethod($className, $methodName);
164
        $docComment .= "\n" . $reflector->getDocComment();
165
        $requires = [];
166
167
        if ($count = \preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) {
168
            foreach (\range(0, $count - 1) as $i) {
169
                $requires[$matches['name'][$i]] = $matches['value'][$i];
170
            }
171
        }
172
173
        if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) {
174
            foreach (\range(0, $count - 1) as $i) {
175
                $requires[$matches['name'][$i]] = [
176
                    'version'  => $matches['version'][$i],
177
                    'operator' => $matches['operator'][$i],
178
                ];
179
            }
180
        }
181
182
        if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $docComment, $matches)) {
183
            foreach (\range(0, $count - 1) as $i) {
184
                if (!empty($requires[$matches['name'][$i]])) {
185
                    continue;
186
                }
187
188
                try {
189
                    $versionConstraintParser = new VersionConstraintParser;
190
191
                    $requires[$matches['name'][$i] . '_constraint'] = [
192
                        'constraint' => $versionConstraintParser->parse(\trim($matches['constraint'][$i])),
193
                    ];
194
                } catch (\PharIo\Version\Exception $e) {
195
                    throw new Warning($e->getMessage(), $e->getCode(), $e);
196
                }
197
            }
198
        }
199
200
        if ($count = \preg_match_all(self::REGEX_REQUIRES_SETTING, $docComment, $matches)) {
201
            $requires['setting'] = [];
202
203
            foreach (\range(0, $count - 1) as $i) {
204
                $requires['setting'][$matches['setting'][$i]] = $matches['value'][$i];
205
            }
206
        }
207
208
        if ($count = \preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) {
209
            foreach (\range(0, $count - 1) as $i) {
210
                $name = $matches['name'][$i] . 's';
211
212
                if (!isset($requires[$name])) {
213
                    $requires[$name] = [];
214
                }
215
216
                $requires[$name][] = $matches['value'][$i];
217
218
                if ($name !== 'extensions' || empty($matches['version'][$i])) {
219
                    continue;
220
                }
221
222
                $requires['extension_versions'][$matches['value'][$i]] = [
223
                    'version'  => $matches['version'][$i],
224
                    'operator' => $matches['operator'][$i],
225
                ];
226
            }
227
        }
228
229
        return $requires;
230
    }
231
232
    /**
233
     * Returns the missing requirements for a test.
234
     *
235
     * @throws Warning
236
     *
237
     * @return string[]
238
     */
239
    public static function getMissingRequirements(string $className, string $methodName): array
240
    {
241
        $required = static::getRequirements($className, $methodName);
242
        $missing  = [];
243
244
        if (!empty($required['PHP'])) {
245
            $operator = empty($required['PHP']['operator']) ? '>=' : $required['PHP']['operator'];
246
247
            if (!\version_compare(\PHP_VERSION, $required['PHP']['version'], $operator)) {
248
                $missing[] = \sprintf('PHP %s %s is required.', $operator, $required['PHP']['version']);
249
            }
250
        } elseif (!empty($required['PHP_constraint'])) {
251
            $version = new \PharIo\Version\Version(self::sanitizeVersionNumber(\PHP_VERSION));
252
253
            if (!$required['PHP_constraint']['constraint']->complies($version)) {
254
                $missing[] = \sprintf(
255
                    'PHP version does not match the required constraint %s.',
256
                    $required['PHP_constraint']['constraint']->asString()
257
                );
258
            }
259
        }
260
261
        if (!empty($required['PHPUnit'])) {
262
            $phpunitVersion = Version::id();
263
264
            $operator = empty($required['PHPUnit']['operator']) ? '>=' : $required['PHPUnit']['operator'];
265
266
            if (!\version_compare($phpunitVersion, $required['PHPUnit']['version'], $operator)) {
267
                $missing[] = \sprintf('PHPUnit %s %s is required.', $operator, $required['PHPUnit']['version']);
268
            }
269
        } elseif (!empty($required['PHPUnit_constraint'])) {
270
            $phpunitVersion = new \PharIo\Version\Version(self::sanitizeVersionNumber(Version::id()));
271
272
            if (!$required['PHPUnit_constraint']['constraint']->complies($phpunitVersion)) {
273
                $missing[] = \sprintf(
274
                    'PHPUnit version does not match the required constraint %s.',
275
                    $required['PHPUnit_constraint']['constraint']->asString()
276
                );
277
            }
278
        }
279
280
        if (!empty($required['OSFAMILY']) && $required['OSFAMILY'] !== (new OperatingSystem)->getFamily()) {
281
            $missing[] = \sprintf('Operating system %s is required.', $required['OSFAMILY']);
282
        }
283
284
        if (!empty($required['OS'])) {
285
            $requiredOsPattern = \sprintf('/%s/i', \addcslashes($required['OS'], '/'));
286
287
            if (!\preg_match($requiredOsPattern, \PHP_OS)) {
288
                $missing[] = \sprintf('Operating system matching %s is required.', $requiredOsPattern);
289
            }
290
        }
291
292
        if (!empty($required['functions'])) {
293
            foreach ($required['functions'] as $function) {
294
                $pieces = \explode('::', $function);
295
296
                if (\count($pieces) === 2 && \class_exists($pieces[0]) && \method_exists($pieces[0], $pieces[1])) {
297
                    continue;
298
                }
299
300
                if (\function_exists($function)) {
301
                    continue;
302
                }
303
304
                $missing[] = \sprintf('Function %s is required.', $function);
305
            }
306
        }
307
308
        if (!empty($required['setting'])) {
309
            foreach ($required['setting'] as $setting => $value) {
310
                if (\ini_get($setting) != $value) {
311
                    $missing[] = \sprintf('Setting "%s" must be "%s".', $setting, $value);
312
                }
313
            }
314
        }
315
316
        if (!empty($required['extensions'])) {
317
            foreach ($required['extensions'] as $extension) {
318
                if (isset($required['extension_versions'][$extension])) {
319
                    continue;
320
                }
321
322
                if (!\extension_loaded($extension)) {
323
                    $missing[] = \sprintf('Extension %s is required.', $extension);
324
                }
325
            }
326
        }
327
328
        if (!empty($required['extension_versions'])) {
329
            foreach ($required['extension_versions'] as $extension => $required) {
330
                $actualVersion = \phpversion($extension);
331
332
                $operator = empty($required['operator']) ? '>=' : $required['operator'];
333
334
                if ($actualVersion === false || !\version_compare($actualVersion, $required['version'], $operator)) {
335
                    $missing[] = \sprintf('Extension %s %s %s is required.', $extension, $operator, $required['version']);
336
                }
337
            }
338
        }
339
340
        return $missing;
341
    }
342
343
    /**
344
     * Returns the expected exception for a test.
345
     *
346
     * @return array|false
347
     */
348
    public static function getExpectedException(string $className, ?string $methodName)
349
    {
350
        $reflector  = new ReflectionMethod($className, $methodName);
351
        $docComment = $reflector->getDocComment();
352
        $docComment = \substr($docComment, 3, -2);
353
354
        if (\preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) {
355
            $annotations = self::parseTestMethodAnnotations(
356
                $className,
357
                $methodName
358
            );
359
360
            $class         = $matches[1];
361
            $code          = null;
362
            $message       = '';
363
            $messageRegExp = '';
364
365
            if (isset($matches[2])) {
366
                $message = \trim($matches[2]);
367
            } elseif (isset($annotations['method']['expectedExceptionMessage'])) {
368
                $message = self::parseAnnotationContent(
369
                    $annotations['method']['expectedExceptionMessage'][0]
370
                );
371
            }
372
373
            if (isset($annotations['method']['expectedExceptionMessageRegExp'])) {
374
                $messageRegExp = self::parseAnnotationContent(
375
                    $annotations['method']['expectedExceptionMessageRegExp'][0]
376
                );
377
            }
378
379
            if (isset($matches[3])) {
380
                $code = $matches[3];
381
            } elseif (isset($annotations['method']['expectedExceptionCode'])) {
382
                $code = self::parseAnnotationContent(
383
                    $annotations['method']['expectedExceptionCode'][0]
384
                );
385
            }
386
387
            if (\is_numeric($code)) {
388
                $code = (int) $code;
389
            } elseif (\is_string($code) && \defined($code)) {
390
                $code = (int) \constant($code);
391
            }
392
393
            return [
394
                'class' => $class, 'code' => $code, 'message' => $message, 'message_regex' => $messageRegExp,
395
            ];
396
        }
397
398
        return false;
399
    }
400
401
    /**
402
     * Returns the provided data for a method.
403
     *
404
     * @throws Exception
405
     */
406
    public static function getProvidedData(string $className, string $methodName): ?array
407
    {
408
        $reflector  = new ReflectionMethod($className, $methodName);
409
        $docComment = $reflector->getDocComment();
410
411
        $data = self::getDataFromDataProviderAnnotation($docComment, $className, $methodName);
412
413
        if ($data === null) {
414
            $data = self::getDataFromTestWithAnnotation($docComment);
415
        }
416
417
        if ($data === []) {
418
            throw new SkippedTestError;
419
        }
420
421
        if ($data !== null) {
422
            foreach ($data as $key => $value) {
423
                if (!\is_array($value)) {
424
                    throw new Exception(
425
                        \sprintf(
426
                            'Data set %s is invalid.',
427
                            \is_int($key) ? '#' . $key : '"' . $key . '"'
428
                        )
429
                    );
430
                }
431
            }
432
        }
433
434
        return $data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data could return the type iterable which is incompatible with the type-hinted return array|null. Consider adding an additional type-check to rule them out.
Loading history...
435
    }
436
437
    /**
438
     * @throws Exception
439
     */
440
    public static function getDataFromTestWithAnnotation(string $docComment): ?array
441
    {
442
        $docComment = self::cleanUpMultiLineAnnotation($docComment);
443
444
        if (\preg_match(self::REGEX_TEST_WITH, $docComment, $matches, \PREG_OFFSET_CAPTURE)) {
445
            $offset            = \strlen($matches[0][0]) + $matches[0][1];
446
            $annotationContent = \substr($docComment, $offset);
447
            $data              = [];
448
449
            foreach (\explode("\n", $annotationContent) as $candidateRow) {
450
                $candidateRow = \trim($candidateRow);
451
452
                if ($candidateRow[0] !== '[') {
453
                    break;
454
                }
455
456
                $dataSet = \json_decode($candidateRow, true);
457
458
                if (\json_last_error() !== \JSON_ERROR_NONE) {
459
                    throw new Exception(
460
                        'The data set for the @testWith annotation cannot be parsed: ' . \json_last_error_msg()
461
                    );
462
                }
463
464
                $data[] = $dataSet;
465
            }
466
467
            if (!$data) {
468
                throw new Exception('The data set for the @testWith annotation cannot be parsed.');
469
            }
470
471
            return $data;
472
        }
473
474
        return null;
475
    }
476
477
    public static function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
478
    {
479
        if (!isset(self::$annotationCache[$className])) {
480
            $class       = new ReflectionClass($className);
481
            $traits      = $class->getTraits();
482
            $annotations = [];
483
484
            foreach ($traits as $trait) {
485
                $annotations = \array_merge(
486
                    $annotations,
487
                    self::parseAnnotations($trait->getDocComment())
488
                );
489
            }
490
491
            self::$annotationCache[$className] = \array_merge(
492
                $annotations,
493
                self::parseAnnotations($class->getDocComment())
494
            );
495
        }
496
497
        $cacheKey = $className . '::' . $methodName;
498
499
        if ($methodName !== null && !isset(self::$annotationCache[$cacheKey])) {
500
            try {
501
                $method      = new ReflectionMethod($className, $methodName);
502
                $annotations = self::parseAnnotations($method->getDocComment());
503
            } catch (ReflectionException $e) {
504
                $annotations = [];
505
            }
506
507
            self::$annotationCache[$cacheKey] = $annotations;
508
        }
509
510
        return [
511
            'class'  => self::$annotationCache[$className],
512
            'method' => $methodName !== null ? self::$annotationCache[$cacheKey] : [],
513
        ];
514
    }
515
516
    public static function getInlineAnnotations(string $className, string $methodName): array
517
    {
518
        $method      = new ReflectionMethod($className, $methodName);
519
        $code        = \file($method->getFileName());
520
        $lineNumber  = $method->getStartLine();
521
        $startLine   = $method->getStartLine() - 1;
522
        $endLine     = $method->getEndLine() - 1;
523
        $methodLines = \array_slice($code, $startLine, $endLine - $startLine + 1);
524
        $annotations = [];
525
526
        foreach ($methodLines as $line) {
527
            if (\preg_match('#/\*\*?\s*@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?\*/$#m', $line, $matches)) {
528
                $annotations[\strtolower($matches['name'])] = [
529
                    'line'  => $lineNumber,
530
                    'value' => $matches['value'],
531
                ];
532
            }
533
534
            $lineNumber++;
535
        }
536
537
        return $annotations;
538
    }
539
540
    public static function parseAnnotations(string $docBlock): array
541
    {
542
        $annotations = [];
543
        // Strip away the docblock header and footer to ease parsing of one line annotations
544
        $docBlock = \substr($docBlock, 3, -2);
545
546
        if (\preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docBlock, $matches)) {
547
            $numMatches = \count($matches[0]);
548
549
            for ($i = 0; $i < $numMatches; ++$i) {
550
                $annotations[$matches['name'][$i]][] = (string) $matches['value'][$i];
551
            }
552
        }
553
554
        return $annotations;
555
    }
556
557
    public static function getBackupSettings(string $className, string $methodName): array
558
    {
559
        return [
560
            'backupGlobals' => self::getBooleanAnnotationSetting(
561
                $className,
562
                $methodName,
563
                'backupGlobals'
564
            ),
565
            'backupStaticAttributes' => self::getBooleanAnnotationSetting(
566
                $className,
567
                $methodName,
568
                'backupStaticAttributes'
569
            ),
570
        ];
571
    }
572
573
    public static function getDependencies(string $className, string $methodName): array
574
    {
575
        $annotations = self::parseTestMethodAnnotations(
576
            $className,
577
            $methodName
578
        );
579
580
        $dependencies = [];
581
582
        if (isset($annotations['class']['depends'])) {
583
            $dependencies = $annotations['class']['depends'];
584
        }
585
586
        if (isset($annotations['method']['depends'])) {
587
            $dependencies = \array_merge(
588
                $dependencies,
589
                $annotations['method']['depends']
590
            );
591
        }
592
593
        return \array_unique($dependencies);
594
    }
595
596
    public static function getErrorHandlerSettings(string $className, ?string $methodName): ?bool
597
    {
598
        return self::getBooleanAnnotationSetting(
599
            $className,
600
            $methodName,
601
            'errorHandler'
602
        );
603
    }
604
605
    public static function getGroups(string $className, ?string $methodName = ''): array
606
    {
607
        $annotations = self::parseTestMethodAnnotations(
608
            $className,
609
            $methodName
610
        );
611
612
        $groups = [];
613
614
        if (isset($annotations['method']['author'])) {
615
            $groups = $annotations['method']['author'];
616
        } elseif (isset($annotations['class']['author'])) {
617
            $groups = $annotations['class']['author'];
618
        }
619
620
        if (isset($annotations['class']['group'])) {
621
            $groups = \array_merge($groups, $annotations['class']['group']);
622
        }
623
624
        if (isset($annotations['method']['group'])) {
625
            $groups = \array_merge($groups, $annotations['method']['group']);
626
        }
627
628
        if (isset($annotations['class']['ticket'])) {
629
            $groups = \array_merge($groups, $annotations['class']['ticket']);
630
        }
631
632
        if (isset($annotations['method']['ticket'])) {
633
            $groups = \array_merge($groups, $annotations['method']['ticket']);
634
        }
635
636
        foreach (['method', 'class'] as $element) {
637
            foreach (['small', 'medium', 'large'] as $size) {
638
                if (isset($annotations[$element][$size])) {
639
                    $groups[] = $size;
640
641
                    break 2;
642
                }
643
            }
644
        }
645
646
        return \array_unique($groups);
647
    }
648
649
    public static function getSize(string $className, ?string $methodName): int
650
    {
651
        $groups = \array_flip(self::getGroups($className, $methodName));
652
653
        if (isset($groups['large'])) {
654
            return self::LARGE;
655
        }
656
657
        if (isset($groups['medium'])) {
658
            return self::MEDIUM;
659
        }
660
661
        if (isset($groups['small'])) {
662
            return self::SMALL;
663
        }
664
665
        return self::UNKNOWN;
666
    }
667
668
    public static function getProcessIsolationSettings(string $className, string $methodName): bool
669
    {
670
        $annotations = self::parseTestMethodAnnotations(
671
            $className,
672
            $methodName
673
        );
674
675
        return isset($annotations['class']['runTestsInSeparateProcesses']) || isset($annotations['method']['runInSeparateProcess']);
676
    }
677
678
    public static function getClassProcessIsolationSettings(string $className, string $methodName): bool
679
    {
680
        $annotations = self::parseTestMethodAnnotations(
681
            $className,
682
            $methodName
683
        );
684
685
        return isset($annotations['class']['runClassInSeparateProcess']);
686
    }
687
688
    public static function getPreserveGlobalStateSettings(string $className, string $methodName): ?bool
689
    {
690
        return self::getBooleanAnnotationSetting(
691
            $className,
692
            $methodName,
693
            'preserveGlobalState'
694
        );
695
    }
696
697
    public static function getHookMethods(string $className): array
698
    {
699
        if (!\class_exists($className, false)) {
700
            return self::emptyHookMethodsArray();
701
        }
702
703
        if (!isset(self::$hookMethods[$className])) {
704
            self::$hookMethods[$className] = self::emptyHookMethodsArray();
705
706
            try {
707
                $class = new ReflectionClass($className);
708
709
                foreach ($class->getMethods() as $method) {
710
                    if ($method->getDeclaringClass()->getName() === Assert::class) {
711
                        continue;
712
                    }
713
714
                    if ($method->getDeclaringClass()->getName() === TestCase::class) {
715
                        continue;
716
                    }
717
718
                    if ($methodComment = $method->getDocComment()) {
719
                        if ($method->isStatic()) {
720
                            if (\strpos($methodComment, '@beforeClass') !== false) {
721
                                \array_unshift(
722
                                    self::$hookMethods[$className]['beforeClass'],
723
                                    $method->getName()
724
                                );
725
                            }
726
727
                            if (\strpos($methodComment, '@afterClass') !== false) {
728
                                self::$hookMethods[$className]['afterClass'][] = $method->getName();
729
                            }
730
                        }
731
732
                        if (\preg_match('/@before\b/', $methodComment) > 0) {
733
                            \array_unshift(
734
                                self::$hookMethods[$className]['before'],
735
                                $method->getName()
736
                            );
737
                        }
738
739
                        if (\preg_match('/@after\b/', $methodComment) > 0) {
740
                            self::$hookMethods[$className]['after'][] = $method->getName();
741
                        }
742
                    }
743
                }
744
            } catch (ReflectionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
745
            }
746
        }
747
748
        return self::$hookMethods[$className];
749
    }
750
751
    /**
752
     * @throws CodeCoverageException
753
     */
754
    private static function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array
755
    {
756
        $annotations = self::parseTestMethodAnnotations(
757
            $className,
758
            $methodName
759
        );
760
761
        $classShortcut = null;
762
763
        if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
764
            if (\count($annotations['class'][$mode . 'DefaultClass']) > 1) {
765
                throw new CodeCoverageException(
766
                    \sprintf(
767
                        'More than one @%sClass annotation in class or interface "%s".',
768
                        $mode,
769
                        $className
770
                    )
771
                );
772
            }
773
774
            $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
775
        }
776
777
        $list = [];
778
779
        if (isset($annotations['class'][$mode])) {
780
            $list = $annotations['class'][$mode];
781
        }
782
783
        if (isset($annotations['method'][$mode])) {
784
            $list = \array_merge($list, $annotations['method'][$mode]);
785
        }
786
787
        $codeList = [];
788
789
        foreach (\array_unique($list) as $element) {
790
            if ($classShortcut && \strncmp($element, '::', 2) === 0) {
791
                $element = $classShortcut . $element;
792
            }
793
794
            $element = \preg_replace('/[\s()]+$/', '', $element);
795
            $element = \explode(' ', $element);
796
            $element = $element[0];
797
798
            if ($mode === 'covers' && \interface_exists($element)) {
799
                throw new InvalidCoversTargetException(
800
                    \sprintf(
801
                        'Trying to @cover interface "%s".',
802
                        $element
803
                    )
804
                );
805
            }
806
807
            $codeList = \array_merge(
808
                $codeList,
809
                self::resolveElementToReflectionObjects($element)
810
            );
811
        }
812
813
        return self::resolveReflectionObjectsToLines($codeList);
814
    }
815
816
    /**
817
     * Parse annotation content to use constant/class constant values
818
     *
819
     * Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME
820
     *
821
     * If the constant is not found the string is used as is to ensure maximum BC.
822
     */
823
    private static function parseAnnotationContent(string $message): string
824
    {
825
        if (\defined($message) && (\strpos($message, '::') !== false && \substr_count($message, '::') + 1 === 2)) {
826
            $message = \constant($message);
827
        }
828
829
        return $message;
830
    }
831
832
    /**
833
     * Returns the provided data for a method.
834
     */
835
    private static function getDataFromDataProviderAnnotation(string $docComment, string $className, string $methodName): ?iterable
836
    {
837
        if (\preg_match_all(self::REGEX_DATA_PROVIDER, $docComment, $matches)) {
838
            $result = [];
839
840
            foreach ($matches[1] as $match) {
841
                $dataProviderMethodNameNamespace = \explode('\\', $match);
842
                $leaf                            = \explode('::', \array_pop($dataProviderMethodNameNamespace));
843
                $dataProviderMethodName          = \array_pop($leaf);
844
845
                if (empty($dataProviderMethodNameNamespace)) {
846
                    $dataProviderMethodNameNamespace = '';
847
                } else {
848
                    $dataProviderMethodNameNamespace = \implode('\\', $dataProviderMethodNameNamespace) . '\\';
849
                }
850
851
                if (empty($leaf)) {
852
                    $dataProviderClassName = $className;
853
                } else {
854
                    $dataProviderClassName = $dataProviderMethodNameNamespace . \array_pop($leaf);
855
                }
856
857
                $dataProviderClass  = new ReflectionClass($dataProviderClassName);
858
                $dataProviderMethod = $dataProviderClass->getMethod(
859
                    $dataProviderMethodName
860
                );
861
862
                if ($dataProviderMethod->isStatic()) {
863
                    $object = null;
864
                } else {
865
                    $object = $dataProviderClass->newInstance();
866
                }
867
868
                if ($dataProviderMethod->getNumberOfParameters() === 0) {
869
                    $data = $dataProviderMethod->invoke($object);
870
                } else {
871
                    $data = $dataProviderMethod->invoke($object, $methodName);
872
                }
873
874
                if ($data instanceof Traversable) {
875
                    $origData = $data;
876
                    $data     = [];
877
878
                    foreach ($origData as $key => $value) {
879
                        if (\is_int($key)) {
880
                            $data[] = $value;
881
                        } else {
882
                            $data[$key] = $value;
883
                        }
884
                    }
885
                }
886
887
                if (\is_array($data)) {
888
                    $result = \array_merge($result, $data);
889
                }
890
            }
891
892
            return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array which is incompatible with the type-hinted return iterable|null.
Loading history...
893
        }
894
895
        return null;
896
    }
897
898
    private static function cleanUpMultiLineAnnotation(string $docComment): string
899
    {
900
        //removing initial '   * ' for docComment
901
        $docComment = \str_replace("\r\n", "\n", $docComment);
902
        $docComment = \preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment);
903
        $docComment = \substr($docComment, 0, -1);
904
905
        return \rtrim($docComment, "\n");
906
    }
907
908
    private static function emptyHookMethodsArray(): array
909
    {
910
        return [
911
            'beforeClass' => ['setUpBeforeClass'],
912
            'before'      => ['setUp'],
913
            'after'       => ['tearDown'],
914
            'afterClass'  => ['tearDownAfterClass'],
915
        ];
916
    }
917
918
    private static function getBooleanAnnotationSetting(string $className, ?string $methodName, string $settingName): ?bool
919
    {
920
        $annotations = self::parseTestMethodAnnotations(
921
            $className,
922
            $methodName
923
        );
924
925
        if (isset($annotations['method'][$settingName])) {
926
            if ($annotations['method'][$settingName][0] === 'enabled') {
927
                return true;
928
            }
929
930
            if ($annotations['method'][$settingName][0] === 'disabled') {
931
                return false;
932
            }
933
        }
934
935
        if (isset($annotations['class'][$settingName])) {
936
            if ($annotations['class'][$settingName][0] === 'enabled') {
937
                return true;
938
            }
939
940
            if ($annotations['class'][$settingName][0] === 'disabled') {
941
                return false;
942
            }
943
        }
944
945
        return null;
946
    }
947
948
    /**
949
     * @throws InvalidCoversTargetException
950
     */
951
    private static function resolveElementToReflectionObjects(string $element): array
952
    {
953
        $codeToCoverList = [];
954
955
        if (\strpos($element, '\\') !== false && \function_exists($element)) {
956
            $codeToCoverList[] = new ReflectionFunction($element);
957
        } elseif (\strpos($element, '::') !== false) {
958
            [$className, $methodName] = \explode('::', $element);
959
960
            if (isset($methodName[0]) && $methodName[0] === '<') {
961
                $classes = [$className];
962
963
                foreach ($classes as $className) {
964
                    if (!\class_exists($className) &&
965
                        !\interface_exists($className) &&
966
                        !\trait_exists($className)) {
967
                        throw new InvalidCoversTargetException(
968
                            \sprintf(
969
                                'Trying to @cover or @use not existing class or ' .
970
                                'interface "%s".',
971
                                $className
972
                            )
973
                        );
974
                    }
975
976
                    $class      = new ReflectionClass($className);
977
                    $methods    = $class->getMethods();
978
                    $inverse    = isset($methodName[1]) && $methodName[1] === '!';
979
                    $visibility = 'isPublic';
980
981
                    if (\strpos($methodName, 'protected')) {
982
                        $visibility = 'isProtected';
983
                    } elseif (\strpos($methodName, 'private')) {
984
                        $visibility = 'isPrivate';
985
                    }
986
987
                    foreach ($methods as $method) {
988
                        if ($inverse && !$method->$visibility()) {
989
                            $codeToCoverList[] = $method;
990
                        } elseif (!$inverse && $method->$visibility()) {
991
                            $codeToCoverList[] = $method;
992
                        }
993
                    }
994
                }
995
            } else {
996
                $classes = [$className];
997
998
                foreach ($classes as $className) {
999
                    if ($className === '' && \function_exists($methodName)) {
1000
                        $codeToCoverList[] = new ReflectionFunction(
1001
                            $methodName
1002
                        );
1003
                    } else {
1004
                        if (!((\class_exists($className) || \interface_exists($className) || \trait_exists($className)) &&
1005
                            \method_exists($className, $methodName))) {
1006
                            throw new InvalidCoversTargetException(
1007
                                \sprintf(
1008
                                    'Trying to @cover or @use not existing method "%s::%s".',
1009
                                    $className,
1010
                                    $methodName
1011
                                )
1012
                            );
1013
                        }
1014
1015
                        $codeToCoverList[] = new ReflectionMethod(
1016
                            $className,
1017
                            $methodName
1018
                        );
1019
                    }
1020
                }
1021
            }
1022
        } else {
1023
            $extended = false;
1024
1025
            if (\strpos($element, '<extended>') !== false) {
1026
                $element  = \str_replace('<extended>', '', $element);
1027
                $extended = true;
1028
            }
1029
1030
            $classes = [$element];
1031
1032
            if ($extended) {
1033
                $classes = \array_merge(
1034
                    $classes,
1035
                    \class_implements($element),
1036
                    \class_parents($element)
1037
                );
1038
            }
1039
1040
            foreach ($classes as $className) {
1041
                if (!\class_exists($className) &&
1042
                    !\interface_exists($className) &&
1043
                    !\trait_exists($className)) {
1044
                    throw new InvalidCoversTargetException(
1045
                        \sprintf(
1046
                            'Trying to @cover or @use not existing class or ' .
1047
                            'interface "%s".',
1048
                            $className
1049
                        )
1050
                    );
1051
                }
1052
1053
                $codeToCoverList[] = new ReflectionClass($className);
1054
            }
1055
        }
1056
1057
        return $codeToCoverList;
1058
    }
1059
1060
    private static function resolveReflectionObjectsToLines(array $reflectors): array
1061
    {
1062
        $result = [];
1063
1064
        foreach ($reflectors as $reflector) {
1065
            if ($reflector instanceof ReflectionClass) {
1066
                foreach ($reflector->getTraits() as $trait) {
1067
                    $reflectors[] = $trait;
1068
                }
1069
            }
1070
        }
1071
1072
        foreach ($reflectors as $reflector) {
1073
            $filename = $reflector->getFileName();
1074
1075
            if (!isset($result[$filename])) {
1076
                $result[$filename] = [];
1077
            }
1078
1079
            $result[$filename] = \array_merge(
1080
                $result[$filename],
1081
                \range($reflector->getStartLine(), $reflector->getEndLine())
1082
            );
1083
        }
1084
1085
        foreach ($result as $filename => $lineNumbers) {
1086
            $result[$filename] = \array_keys(\array_flip($lineNumbers));
1087
        }
1088
1089
        return $result;
1090
    }
1091
1092
    /**
1093
     * Trims any extensions from version string that follows after
1094
     * the <major>.<minor>[.<patch>] format
1095
     */
1096
    private static function sanitizeVersionNumber(string $version)
1097
    {
1098
        return \preg_replace(
1099
            '/^(\d+\.\d+(?:.\d+)?).*$/',
1100
            '$1',
1101
            $version
1102
        );
1103
    }
1104
1105
    private static function shouldCoversAnnotationBeUsed(array $annotations): bool
1106
    {
1107
        if (isset($annotations['method']['coversNothing'])) {
1108
            return false;
1109
        }
1110
1111
        if (isset($annotations['method']['covers'])) {
1112
            return true;
1113
        }
1114
1115
        if (isset($annotations['class']['coversNothing'])) {
1116
            return false;
1117
        }
1118
1119
        return true;
1120
    }
1121
}
1122