Passed
Pull Request — master (#47)
by Pascal
15:06 queued 27s
created

DocBlock   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 212
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 21
eloc 81
c 1
b 0
f 0
dl 0
loc 212
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A symbolAnnotations() 0 3 1
A getInlineAnnotations() 0 25 4
A __construct() 0 18 1
A ofMethod() 0 19 4
A parseDocBlock() 0 15 3
A ofClass() 0 21 4
A extractAnnotationsFromReflector() 0 23 4
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHPUnit.
7
 *
8
 * (c) Sebastian Bergmann <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace FriendsOfPhpSpec\PhpSpec\CodeCoverage\Annotation;
15
16
use Exception;
17
use ReflectionClass;
18
use ReflectionMethod;
19
use Reflector;
20
21
use function array_map;
22
use function array_merge;
23
use function array_slice;
24
use function array_values;
25
use function count;
26
use function file;
27
use function preg_match;
28
use function preg_match_all;
29
use function strtolower;
30
use function substr;
31
32
/**
33
 * This is an abstraction around a PHPUnit-specific docBlock,
34
 * allowing us to ask meaningful questions about a specific
35
 * reflection symbol.
36
 *
37
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
38
 */
39
final class DocBlock
40
{
41
    /**
42
     * @var string
43
     */
44
    private $className;
45
46
    /**
47
     * @var string
48
     */
49
    private $docComment;
50
51
    /**
52
     * @var int
53
     */
54
    private $endLine;
55
56
    /**
57
     * @var string
58
     */
59
    private $fileName;
60
61
    /**
62
     * @var bool
63
     */
64
    private $isMethod;
65
66
    /**
67
     * @var string
68
     */
69
    private $name;
70
71
    /**
72
     * @var int
73
     */
74
    private $startLine;
75
76
    /**
77
     * @var array<string, array<int, string>> pre-parsed annotations indexed by name and occurrence index
78
     */
79
    private $symbolAnnotations;
80
81
    /**
82
     * Note: we do not preserve an instance of the reflection object, since it cannot be safely (de-)serialized.
83
     *
84
     * @param array<string, array<int, string>> $symbolAnnotations
85
     */
86
    private function __construct(
87
        string $docComment,
88
        bool $isMethod,
89
        array $symbolAnnotations,
90
        int $startLine,
91
        int $endLine,
92
        string $fileName,
93
        string $name,
94
        string $className
95
    ) {
96
        $this->docComment = $docComment;
97
        $this->isMethod = $isMethod;
98
        $this->symbolAnnotations = $symbolAnnotations;
99
        $this->startLine = $startLine;
100
        $this->endLine = $endLine;
101
        $this->fileName = $fileName;
102
        $this->name = $name;
103
        $this->className = $className;
104
    }
105
106
    /**
107
     * @throws Exception
108
     *
109
     * @return array<string, array>
110
     */
111
    public function getInlineAnnotations(): array
112
    {
113
        if (false === $code = file($this->fileName)) {
114
            throw new Exception(sprintf('Could not read file `%s`', $this->fileName));
115
        }
116
117
        $lineNumber = $this->startLine;
118
        $startLine = $this->startLine - 1;
119
        $endLine = $this->endLine - 1;
120
        $codeLines = array_slice($code, $startLine, $endLine - $startLine + 1);
121
        $annotations = [];
122
        $pattern = '#/\*\*?\s*@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?\*/$#m';
123
124
        foreach ($codeLines as $line) {
125
            if (preg_match($pattern, $line, $matches)) {
126
                $annotations[strtolower($matches['name'])] = [
127
                    'line' => $lineNumber,
128
                    'value' => $matches['value'],
129
                ];
130
            }
131
132
            ++$lineNumber;
133
        }
134
135
        return $annotations;
136
    }
137
138
    /**
139
     * @param ReflectionClass<object> $class
140
     *
141
     * @throws Exception
142
     *
143
     * @return static
144
     */
145
    public static function ofClass(ReflectionClass $class): self
146
    {
147
        $className = $class->getName();
148
149
        $startLine = $class->getStartLine();
150
        $endLine = $class->getEndLine();
151
        $fileName = $class->getFileName();
152
153
        if (false === $startLine || false === $endLine || false === $fileName) {
154
            throw new Exception('Could not get required information from class');
155
        }
156
157
        return new self(
158
            (string) $class->getDocComment(),
159
            false,
160
            self::extractAnnotationsFromReflector($class),
161
            $startLine,
162
            $endLine,
163
            $fileName,
164
            $className,
165
            $className
166
        );
167
    }
168
169
    /**
170
     * @throws Exception
171
     *
172
     * @return static
173
     */
174
    public static function ofMethod(ReflectionMethod $method, string $classNameInHierarchy): self
175
    {
176
        $startLine = $method->getStartLine();
177
        $endLine = $method->getEndLine();
178
        $fileName = $method->getFileName();
179
180
        if (false === $startLine || false === $endLine || false === $fileName) {
181
            throw new Exception('Could not get required information from class');
182
        }
183
184
        return new self(
185
            (string) $method->getDocComment(),
186
            true,
187
            self::extractAnnotationsFromReflector($method),
188
            $startLine,
189
            $endLine,
190
            $fileName,
191
            $method->getName(),
192
            $classNameInHierarchy
193
        );
194
    }
195
196
    /**
197
     * @return array<string, array<int, string>>
198
     */
199
    public function symbolAnnotations(): array
200
    {
201
        return $this->symbolAnnotations;
202
    }
203
204
    /**
205
     * @return array<string, array>
206
     */
207
    private static function extractAnnotationsFromReflector(Reflector $reflector): array
208
    {
209
        $annotations = [];
210
211
        if ($reflector instanceof ReflectionClass) {
212
            $annotations = array_merge(
213
                $annotations,
214
                ...array_map(
215
                    static function (ReflectionClass $trait): array {
216
                        return self::parseDocBlock((string) $trait->getDocComment());
217
                    },
218
                    array_values($reflector->getTraits())
219
                )
220
            );
221
        }
222
223
        if (!$reflector instanceof ReflectionClass && !$reflector instanceof ReflectionMethod) {
224
            return $annotations;
225
        }
226
227
        return array_merge(
228
            $annotations,
229
            self::parseDocBlock((string) $reflector->getDocComment())
230
        );
231
    }
232
233
    /**
234
     * @return array<array<string>>
235
     */
236
    private static function parseDocBlock(string $docBlock): array
237
    {
238
        // Strip away the docblock header and footer to ease parsing of one line annotations
239
        $docBlock = (string) substr($docBlock, 3, -2);
240
        $annotations = [];
241
242
        if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docBlock, $matches)) {
243
            $numMatches = count($matches[0]);
244
245
            for ($i = 0; $i < $numMatches; ++$i) {
246
                $annotations[$matches['name'][$i]][] = (string) $matches['value'][$i];
247
            }
248
        }
249
250
        return $annotations;
251
    }
252
}
253