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

CoversAnnotationUtil   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Importance

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

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A getLinesToBeCovered() 0 12 2
A shouldCoversAnnotationBeUsed() 0 15 4
A parseTestMethodAnnotations() 0 16 3
B getLinesToBeCoveredOrUsed() 0 66 10
A getLinesToBeUsed() 0 3 1
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 SebastianBergmann\CodeUnit\CodeUnitCollection;
10
use SebastianBergmann\CodeUnit\InvalidCodeUnitException;
11
use SebastianBergmann\CodeUnit\Mapper;
12
13
final class CoversAnnotationUtil
14
{
15
    /**
16
     * @var Registry
17
     */
18
    private $registry;
19
20
    public function __construct(Registry $registry)
21
    {
22
        $this->registry = $registry;
23
    }
24
25
    /**
26
     * @throws CodeCoverageException
27
     *
28
     * @return array|bool
29
     */
30
    public function getLinesToBeCovered(string $className, string $methodName)
31
    {
32
        $annotations = self::parseTestMethodAnnotations(
0 ignored issues
show
Bug Best Practice introduced by
The method FriendsOfPhpSpec\PhpSpec...TestMethodAnnotations() is not static, but was called statically. ( Ignorable by Annotation )

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

32
        /** @scrutinizer ignore-call */ 
33
        $annotations = self::parseTestMethodAnnotations(
Loading history...
33
            $className,
34
            $methodName
35
        );
36
37
        if (!$this->shouldCoversAnnotationBeUsed($annotations)) {
38
            return false;
39
        }
40
41
        return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
42
    }
43
44
    /**
45
     * Returns lines of code specified with the @uses annotation.
46
     *
47
     * @throws CodeCoverageException
48
     */
49
    public function getLinesToBeUsed(string $className, string $methodName): array
50
    {
51
        return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
52
    }
53
54
    public function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
55
    {
56
        if ($methodName !== null) {
57
            try {
58
                return [
59
                    'method' => $this->registry->forMethod($className, $methodName)->symbolAnnotations(),
60
                    'class'  => $this->registry->forClassName($className)->symbolAnnotations(),
61
                ];
62
            } catch (\ReflectionException $methodNotFound) {
63
                // ignored
64
            }
65
        }
66
67
        return [
68
            'method' => null,
69
            'class' => $this->registry->forClassName($className)->symbolAnnotations(),
70
        ];
71
    }
72
73
    /**
74
     * @param string $className
75
     * @param string $methodName
76
     * @param string $mode
77
     * @return array
78
     * @throws CodeCoverageException
79
     */
80
    private function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array
81
    {
82
        $annotations = $this->parseTestMethodAnnotations(
83
            $className,
84
            $methodName
85
        );
86
87
        $classShortcut = null;
88
89
        if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
90
            if (count($annotations['class'][$mode . 'DefaultClass']) > 1) {
91
                throw new CodeCoverageException(
92
                    sprintf(
93
                        'More than one @%sClass annotation in class or interface "%s".',
94
                        $mode,
95
                        $className
96
                    )
97
                );
98
            }
99
100
            $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
101
        }
102
103
        $list = $annotations['class'][$mode] ?? [];
104
105
        if (isset($annotations['method'][$mode])) {
106
            $list = array_merge($list, $annotations['method'][$mode]);
107
        }
108
109
        $codeUnits = CodeUnitCollection::fromArray([]);
110
        $mapper = new Mapper();
111
112
        foreach (array_unique($list) as $element) {
113
            if ($classShortcut && strncmp($element, '::', 2) === 0) {
114
                $element = $classShortcut . $element;
115
            }
116
117
            $element = preg_replace('/[\s()]+$/', '', $element);
118
            $element = explode(' ', $element);
119
            $element = $element[0];
120
121
            if ($mode === 'covers' && interface_exists($element)) {
122
                throw new InvalidCoversTargetException(
123
                    sprintf(
124
                        'Trying to @cover interface "%s".',
125
                        $element
126
                    )
127
                );
128
            }
129
130
            try {
131
                $codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($element));
132
            } catch (InvalidCodeUnitException $e) {
133
                throw new InvalidCoversTargetException(
134
                    sprintf(
135
                        '"@%s %s" is invalid',
136
                        $mode,
137
                        $element
138
                    ),
139
                    (int) $e->getCode(),
140
                    $e
141
                );
142
            }
143
        }
144
145
        return $mapper->codeUnitsToSourceLines($codeUnits);
146
    }
147
148
    private function shouldCoversAnnotationBeUsed(array $annotations): bool
149
    {
150
        if (isset($annotations['method']['coversNothing'])) {
151
            return false;
152
        }
153
154
        if (isset($annotations['method']['covers'])) {
155
            return true;
156
        }
157
158
        if (isset($annotations['class']['coversNothing'])) {
159
            return false;
160
        }
161
162
        return true;
163
    }
164
}
165