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

CoversAnnotationUtil::getLinesToBeUsed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace FriendsOfPhpSpec\PhpSpec\CodeCoverage\Annotation;
6
7
use FriendsOfPhpSpec\PhpSpec\CodeCoverage\Exception\CodeCoverageException;
8
use FriendsOfPhpSpec\PhpSpec\CodeCoverage\Exception\InvalidCoversTargetException;
9
use ReflectionException;
10
use SebastianBergmann\CodeUnit\CodeUnitCollection;
11
use SebastianBergmann\CodeUnit\InvalidCodeUnitException;
12
use SebastianBergmann\CodeUnit\Mapper;
13
14
use function count;
15
16
final class CoversAnnotationUtil
17
{
18
    /**
19
     * @var Registry
20
     */
21
    private $registry;
22
23
    public function __construct(Registry $registry)
24
    {
25
        $this->registry = $registry;
26
    }
27
28
    /**
29
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
30
     *
31
     * @throws CodeCoverageException
32
     * @throws InvalidCoversTargetException
33
     * @throws ReflectionException
34
     *
35
     * @return array<string, array>|false
36
     */
37
    public function getLinesToBeCovered(string $className, string $methodName)
38
    {
39
        $annotations = $this->parseTestMethodAnnotations(
40
            $className,
41
            $methodName
42
        );
43
44
        if (!$this->shouldCoversAnnotationBeUsed($annotations)) {
45
            return false;
46
        }
47
48
        return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
49
    }
50
51
    /**
52
     * Returns lines of code specified with the.
53
     *
54
     * @param class-string $className .
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
55
     *
56
     * @throws CodeCoverageException
57
     * @throws InvalidCoversTargetException
58
     * @throws ReflectionException
59
     *
60
     * @return array<string, array>
61
     *
62
     * @uses annotation.
63
     */
64
    public function getLinesToBeUsed(string $className, string $methodName): array
65
    {
66
        return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
67
    }
68
69
    /**
70
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
71
     *
72
     * @throws ReflectionException
73
     *
74
     * @return array<string, mixed>
75
     */
76
    public function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
77
    {
78
        if (null !== $methodName) {
79
            try {
80
                return [
81
                    'method' => $this->registry->forMethod($className, $methodName)->symbolAnnotations(),
82
                    'class' => $this->registry->forClassName($className)->symbolAnnotations(),
83
                ];
84
            } catch (ReflectionException $methodNotFound) {
85
                // ignored
86
            }
87
        }
88
89
        return [
90
            'method' => null,
91
            'class' => $this->registry->forClassName($className)->symbolAnnotations(),
92
        ];
93
    }
94
95
    /**
96
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
97
     *
98
     * @throws CodeCoverageException
99
     * @throws InvalidCoversTargetException
100
     * @throws ReflectionException
101
     *
102
     * @return array<string, array>
103
     */
104
    private function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array
105
    {
106
        $annotations = $this->parseTestMethodAnnotations(
107
            $className,
108
            $methodName
109
        );
110
111
        $classShortcut = null;
112
113
        if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
114
            if (count($annotations['class'][$mode . 'DefaultClass']) > 1) {
115
                throw new CodeCoverageException(
116
                    sprintf(
117
                        'More than one @%sClass annotation in class or interface "%s".',
118
                        $mode,
119
                        $className
120
                    )
121
                );
122
            }
123
124
            $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
125
        }
126
127
        $list = $annotations['class'][$mode] ?? [];
128
129
        if (isset($annotations['method'][$mode])) {
130
            $list = array_merge($list, $annotations['method'][$mode]);
131
        }
132
133
        $codeUnits = CodeUnitCollection::fromArray([]);
134
        $mapper = new Mapper();
135
136
        foreach (array_unique($list) as $element) {
137
            if ($classShortcut && strncmp($element, '::', 2) === 0) {
138
                $element = $classShortcut . $element;
139
            }
140
141
            $element = preg_replace('/[\s()]+$/', '', $element);
142
            $element = explode(' ', $element);
143
            $element = $element[0];
144
145
            if ('covers' === $mode && interface_exists($element)) {
146
                throw new InvalidCoversTargetException(
147
                    sprintf(
148
                        'Trying to @cover interface "%s".',
149
                        $element
150
                    )
151
                );
152
            }
153
154
            try {
155
                $codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($element));
156
            } catch (InvalidCodeUnitException $e) {
157
                throw new InvalidCoversTargetException(
158
                    sprintf(
159
                        '"@%s %s" is invalid',
160
                        $mode,
161
                        $element
162
                    ),
163
                    (int) $e->getCode(),
164
                    $e
165
                );
166
            }
167
        }
168
169
        return $mapper->codeUnitsToSourceLines($codeUnits);
170
    }
171
172
    /**
173
     * @param array<string, array<string, mixed>> $annotations
174
     */
175
    private function shouldCoversAnnotationBeUsed(array $annotations): bool
176
    {
177
        if (isset($annotations['method']['coversNothing'])) {
178
            return false;
179
        }
180
181
        if (isset($annotations['method']['covers'])) {
182
            return true;
183
        }
184
185
        if (isset($annotations['class']['coversNothing'])) {
186
            return false;
187
        }
188
189
        return true;
190
    }
191
}
192