Completed
Push — master ( 4ffa9b...a4fcf1 )
by Julian
12s
created

Parser::getMethods()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
ccs 11
cts 11
cp 1
rs 8.8571
cc 5
eloc 11
nc 9
nop 0
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ParaTest\Parser;
6
7
class Parser
8
{
9
    /**
10
     * The path to the source file to parse.
11
     *
12
     * @var string
13
     */
14
    private $path;
15
16
    /**
17
     * @var \ReflectionClass
18
     */
19
    private $refl;
20
21
    /**
22
     * Matches a test method beginning with the conventional "test"
23
     * word.
24
     *
25
     * @var string
26
     */
27
    private static $testName = '/^test/';
28
29
    /**
30
     * A pattern for matching test methods that use the @test annotation.
31
     *
32
     * @var string
33
     */
34
    private static $testAnnotation = '/@test\b/';
35
36 31
    public function __construct($srcPath)
37
    {
38 31
        if (!file_exists($srcPath)) {
39 1
            throw new \InvalidArgumentException('file not found: ' . $srcPath);
40
        }
41
42 30
        $this->path = $srcPath;
43 30
        $declaredClasses = get_declared_classes();
44 30
        require_once $this->path;
45 30
        $class = $this->getClassName($this->path, $declaredClasses);
46 30
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
47 2
            throw new NoClassInFileException();
48
        }
49
        try {
50 28
            $this->refl = new \ReflectionClass($class);
51
        } catch (\ReflectionException $e) {
52
            throw new \InvalidArgumentException('Unable to instantiate ReflectionClass. ' . $class . ' not found in: ' . $srcPath);
53
        }
54 28
    }
55
56
    /**
57
     * Returns the fully constructed class
58
     * with methods or null if the class is abstract.
59
     *
60
     * @return null|ParsedClass
61
     */
62 28
    public function getClass()
63
    {
64 28
        return ($this->refl->isAbstract())
65
            ? null
66 28
            : new ParsedClass(
67 28
                $this->refl->getDocComment(),
68 28
                $this->getCleanReflectionName(),
69 28
                $this->refl->getNamespaceName(),
70 28
                $this->getMethods()
71
            );
72
    }
73
74
    /**
75
     * Return reflection name with null bytes stripped.
76
     *
77
     * @return string
78
     */
79 28
    private function getCleanReflectionName()
80
    {
81 28
        return str_replace("\x00", '', $this->refl->getName());
82
    }
83
84
    /**
85
     * Return all test methods present in the file.
86
     *
87
     * @return array
88
     */
89 28
    private function getMethods()
90
    {
91 28
        $tests = [];
92 28
        $methods = $this->refl->getMethods(\ReflectionMethod::IS_PUBLIC);
93 28
        foreach ($methods as $method) {
94 28
            $hasTestName = preg_match(self::$testName, $method->getName());
95 28
            $docComment = $method->getDocComment();
96 28
            $hasTestAnnotation = false !== $docComment && preg_match(self::$testAnnotation, $docComment);
97 28
            $isTestMethod = $hasTestName || $hasTestAnnotation;
98 28
            if ($isTestMethod) {
99 28
                $tests[] = new ParsedFunction($method->getDocComment(), 'public', $method->getName());
100
            }
101
        }
102
103 28
        return $tests;
104
    }
105
106
    /**
107
     * Return the class name of the class contained
108
     * in the file.
109
     *
110
     * @param mixed $filename
111
     * @param mixed $previousDeclaredClasses
112
     *
113
     * @return string
114
     */
115 30
    private function getClassName($filename, $previousDeclaredClasses)
116
    {
117 30
        $filename = realpath($filename);
118 30
        $classes = get_declared_classes();
119 30
        $newClasses = array_values(array_diff($classes, $previousDeclaredClasses));
120
121 30
        $className = $this->_searchForUnitTestClass($newClasses, $filename);
122 30
        if (isset($className)) {
123 12
            return $className;
124
        }
125
126 22
        $className = $this->_searchForUnitTestClass($classes, $filename);
127 22
        if (isset($className)) {
128 20
            return $className;
129
        }
130 2
    }
131
132
    /**
133
     * Search for the name of the unit test.
134
     *
135
     * @param string[] $classes
136
     * @param string   $filename
137
     *
138
     * @return string|null
139
     */
140 30
    private function _searchForUnitTestClass(array $classes, $filename)
141
    {
142
        // TODO: After merging this PR or other PR for phpunit 6 support, keep only the applicable subclass name
143 30
        $matchingClassName = null;
144 30
        foreach ($classes as $className) {
145 30
            $class = new \ReflectionClass($className);
146 30
            if ($class->getFileName() === $filename) {
147 28
                if ($class->isSubclassOf('PHPUnit\Framework\TestCase')) {
148 28
                    if ($this->classNameMatchesFileName($filename, $className)) {
149 21
                        return $className;
150 18
                    } elseif ($matchingClassName === null) {
151 27
                        $matchingClassName = $className;
152
                    }
153
                }
154
            }
155
        }
156
157 25
        return $matchingClassName;
158
    }
159
160
    /**
161
     * @param $filename
162
     * @param $className
163
     *
164
     * @return bool
165
     */
166 28
    private function classNameMatchesFileName($filename, $className)
167
    {
168 28
        return strpos($filename, $className) !== false
169 28
            || strpos($filename, $this->invertSlashes($className)) !== false;
170
    }
171
172 18
    private function invertSlashes($className)
173
    {
174 18
        return str_replace('\\', '/', $className);
175
    }
176
}
177