Passed
Pull Request — master (#47)
by Pascal
14:53
created

DocBlock::parseDocBlock()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 15
rs 10
cc 3
nc 2
nop 1
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
namespace FriendsOfPhpSpec\PhpSpec\CodeCoverage\Annotation;
14
15
use function array_map;
16
use function array_merge;
17
use function array_slice;
18
use function array_values;
19
use function count;
20
use function file;
21
use function preg_match;
22
use function preg_match_all;
23
use function strtolower;
24
use function substr;
25
use ReflectionClass;
26
use ReflectionFunctionAbstract;
27
use ReflectionMethod;
28
use Reflector;
29
30
/**
31
 * This is an abstraction around a PHPUnit-specific docBlock,
32
 * allowing us to ask meaningful questions about a specific
33
 * reflection symbol.
34
 *
35
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
36
 */
37
final class DocBlock
38
{
39
    /** @var string */
40
    private $docComment;
41
42
    /** @var bool */
43
    private $isMethod;
44
45
    /** @var array<string, array<int, string>> pre-parsed annotations indexed by name and occurrence index */
46
    private $symbolAnnotations;
47
48
    /** @var int */
49
    private $startLine;
50
51
    /** @var int */
52
    private $endLine;
53
54
    /** @var string */
55
    private $fileName;
56
57
    /** @var string */
58
    private $name;
59
60
    /**
61
     * @var string
62
     *
63
     * @psalm-var class-string
64
     */
65
    private $className;
66
67
    public static function ofClass(ReflectionClass $class): self
68
    {
69
        $className = $class->getName();
70
71
        return new self(
72
            (string) $class->getDocComment(),
73
            false,
74
            self::extractAnnotationsFromReflector($class),
75
            $class->getStartLine(),
76
            $class->getEndLine(),
77
            $class->getFileName(),
78
            $className,
79
            $className
80
        );
81
    }
82
83
    /**
84
     * @psalm-param class-string $classNameInHierarchy
85
     */
86
    public static function ofMethod(ReflectionMethod $method, string $classNameInHierarchy): self
87
    {
88
        return new self(
89
            (string) $method->getDocComment(),
90
            true,
91
            self::extractAnnotationsFromReflector($method),
92
            $method->getStartLine(),
93
            $method->getEndLine(),
94
            $method->getFileName(),
95
            $method->getName(),
96
            $classNameInHierarchy
97
        );
98
    }
99
100
    /**
101
     * Note: we do not preserve an instance of the reflection object, since it cannot be safely (de-)serialized.
102
     *
103
     * @param string $docComment
104
     * @param bool $isMethod
105
     * @param array<string, array<int, string>> $symbolAnnotations
106
     * @param int $startLine
107
     * @param int $endLine
108
     * @param string $fileName
109
     * @param string $name
110
     * @param string $className
111
     */
112
    private function __construct(
113
        string $docComment,
114
        bool $isMethod,
115
        array $symbolAnnotations,
116
        int $startLine,
117
        int $endLine,
118
        string $fileName,
119
        string $name,
120
        string $className
121
    ) {
122
        $this->docComment        = $docComment;
123
        $this->isMethod          = $isMethod;
124
        $this->symbolAnnotations = $symbolAnnotations;
125
        $this->startLine         = $startLine;
126
        $this->endLine           = $endLine;
127
        $this->fileName          = $fileName;
128
        $this->name              = $name;
129
        $this->className         = $className;
130
    }
131
132
    /**
133
     * @psalm-return array<string, array{line: int, value: string}>
134
     */
135
    public function getInlineAnnotations(): array
136
    {
137
        $code        = file($this->fileName);
138
        $lineNumber  = $this->startLine;
139
        $startLine   = $this->startLine - 1;
140
        $endLine     = $this->endLine - 1;
141
        $codeLines   = array_slice($code, $startLine, $endLine - $startLine + 1);
142
        $annotations = [];
143
144
        foreach ($codeLines as $line) {
145
            if (preg_match('#/\*\*?\s*@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?\*/$#m', $line, $matches)) {
146
                $annotations[strtolower($matches['name'])] = [
147
                    'line'  => $lineNumber,
148
                    'value' => $matches['value'],
149
                ];
150
            }
151
152
            $lineNumber++;
153
        }
154
155
        return $annotations;
156
    }
157
158
    public function symbolAnnotations(): array
159
    {
160
        return $this->symbolAnnotations;
161
    }
162
163
    /**
164
     * @param string $docBlock
165
     * @return array<string, array<int, string>>
166
     */
167
    private static function parseDocBlock(string $docBlock): array
168
    {
169
        // Strip away the docblock header and footer to ease parsing of one line annotations
170
        $docBlock    = (string) substr($docBlock, 3, -2);
171
        $annotations = [];
172
173
        if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docBlock, $matches)) {
174
            $numMatches = count($matches[0]);
175
176
            for ($i = 0; $i < $numMatches; $i++) {
177
                $annotations[$matches['name'][$i]][] = (string) $matches['value'][$i];
178
            }
179
        }
180
181
        return $annotations;
182
    }
183
184
    /** 
185
     * @param ReflectionClass|ReflectionFunctionAbstract $reflector 
186
     * @return array
187
     */
188
    private static function extractAnnotationsFromReflector(Reflector $reflector): array
189
    {
190
        $annotations = [];
191
192
        if ($reflector instanceof ReflectionClass) {
193
            $annotations = array_merge(
194
                $annotations,
195
                ...array_map(
196
                    static function (ReflectionClass $trait): array {
197
                        return self::parseDocBlock((string) $trait->getDocComment());
198
                    },
199
                    array_values($reflector->getTraits())
200
                )
201
            );
202
        }
203
204
        return array_merge(
205
            $annotations,
206
            self::parseDocBlock((string) $reflector->getDocComment())
0 ignored issues
show
Bug introduced by
The method getDocComment() does not exist on Reflector. It seems like you code against a sub-type of said class. However, the method does not exist in ReflectionExtension or ReflectionZendExtension or ReflectionParameter. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

206
            self::parseDocBlock((string) $reflector->/** @scrutinizer ignore-call */ getDocComment())
Loading history...
207
        );
208
    }
209
}
210