Completed
Push — master ( 0e4742...25fc63 )
by brian
05:15
created

HyphenatedRangeParser::parse()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 6.0146

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 25
cts 27
cp 0.9259
rs 8.4751
c 0
b 0
f 0
cc 6
eloc 26
nc 6
nop 1
crap 6.0146
1
<?php
2
3
/**
4
 * @copyright   (c) 2014-2017 brian ridley
5
 * @author      brian ridley <[email protected]>
6
 * @license     http://opensource.org/licenses/MIT MIT
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ptlis\SemanticVersion\Parse\Matcher;
13
14
use ptlis\SemanticVersion\Comparator\ComparatorInterface;
15
use ptlis\SemanticVersion\Parse\Token;
16
use ptlis\SemanticVersion\Version\Label\LabelBuilder;
17
use ptlis\SemanticVersion\Version\Version;
18
use ptlis\SemanticVersion\Version\VersionBuilder;
19
use ptlis\SemanticVersion\VersionRange\ComparatorVersion;
20
use ptlis\SemanticVersion\VersionRange\LogicalAnd;
21
use ptlis\SemanticVersion\VersionRange\VersionRangeInterface;
22
23
/**
24
 * Parser for hyphenated ranges.
25
 *
26
 * Hyphenated ranges are implemented as described @ https://getcomposer.org/doc/articles/versions.md#range-hyphen-
27
 */
28
final class HyphenatedRangeParser implements RangeParserInterface
29
{
30
    /**
31
     * @var VersionBuilder
32
     */
33
    private $versionBuilder;
34
35
    /**
36
     * @var ComparatorInterface
37
     */
38
    private $greaterOrEqualTo;
39
40
    /**
41
     * @var ComparatorInterface
42
     */
43
    private $lessThan;
44
45
    /**
46
     * @var ComparatorInterface
47
     */
48
    private $lessOrEqualTo;
49
50
51
    /**
52
     * Constructor.
53
     *
54
     * @param VersionBuilder $versionBuilder
55
     * @param ComparatorInterface $greaterOrEqualTo
56
     * @param ComparatorInterface $lessThan
57
     * @param ComparatorInterface $lessOrEqualTo
58
     */
59 11
    public function __construct(
60
        VersionBuilder $versionBuilder,
61
        ComparatorInterface $greaterOrEqualTo,
62
        ComparatorInterface $lessThan,
63
        ComparatorInterface $lessOrEqualTo
64
    ) {
65 11
        $this->versionBuilder = $versionBuilder;
66 11
        $this->greaterOrEqualTo = $greaterOrEqualTo;
67 11
        $this->lessThan = $lessThan;
68 11
        $this->lessOrEqualTo = $lessOrEqualTo;
69 11
    }
70
71
    /**
72
     * Returns true if the token list can be parsed as a hyphenated range.
73
     *
74
     * @param Token[] $tokenList
75
     *
76
     * @return boolean
77
     */
78 11
    public function canParse(array $tokenList)
79
    {
80
        $validConfigurations = [
81 11
            [Token::DIGITS, Token::DIGITS],
82 11
            [Token::DIGITS, Token::LABEL_STRING, Token::DIGITS],
83 11
            [Token::DIGITS, Token::DIGITS, Token::LABEL_STRING],
84 11
            [Token::DIGITS, Token::LABEL_STRING, Token::DIGITS, Token::LABEL_STRING]
85 11
        ];
86
87 11
        $isRange = false;
88 11
        $chunkedList = $this->chunk($tokenList);
89 11
        foreach ($validConfigurations as $configuration) {
90 11
            $isRange = $isRange || $this->chunksMatchConfiguration($chunkedList, $configuration);
91 11
        }
92
93 11
        return $isRange;
94
    }
95
96
    /**
97
     * Returns true if the provided token
98
     *
99
     * @param Token[][] $chunkedList
100
     * @param string[] $configuration
101
     *
102
     * @return boolean
103
     */
104 11
    private function chunksMatchConfiguration(
105
        array $chunkedList,
106
        array $configuration
107
    ) {
108 11
        $matches = count($chunkedList) === count($configuration);
109
110 11
        foreach ($configuration as $index => $token) {
111 11
            if ($matches) {
112 9
                $matches = $chunkedList[$index][0]->getType() === $token;
113 9
            }
114 11
        }
115
116 11
        return $matches;
117
    }
118
119
    /**
120
     * Build a ComparatorVersion representing the hyphenated range.
121
     *
122
     * @param Token[] $tokenList
123
     *
124
     * @return VersionRangeInterface
125
     */
126 10
    public function parse(array $tokenList)
127
    {
128 10
        if (!$this->canParse($tokenList)) {
129 4
            throw new \RuntimeException('Invalid version');
130
        }
131
132 6
        $chunkedList = $this->chunk($tokenList);
133
134 6
        switch (count($chunkedList)) {
1 ignored issue
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
135
136
            // No labels
137 6
            case 2:
138 3
                $lowerVersionConstraint = $this->getLowerConstraint($chunkedList[0]);
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $lowerVersionConstraint exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
139 3
                $upperVersionConstraint = $this->getUpperConstraint($chunkedList[1]);
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $upperVersionConstraint exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
140 3
                break;
141
142
            // Label on one version
143 3
            case 3:
144
                // Label belongs to first version
145 2
                if (Token::LABEL_STRING === $chunkedList[1][0]->getType()) {
146 1
                    $lowerVersionConstraint = $this->getLowerConstraint($chunkedList[0], $chunkedList[1]);
147 1
                    $upperVersionConstraint = $this->getUpperConstraint($chunkedList[2]);
148
149
                // Label belongs to second version
150 1
                } else {
151 1
                    $lowerVersionConstraint = $this->getLowerConstraint($chunkedList[0]);
152 1
                    $upperVersionConstraint = $this->getUpperConstraint($chunkedList[1], $chunkedList[2]);
153
                }
154
155 2
                break;
156
157
            // Label on both versions
158 1
            case 4:
159 1
                $lowerVersionConstraint = $this->getLowerConstraint($chunkedList[0], $chunkedList[1]);
160 1
                $upperVersionConstraint = $this->getUpperConstraint($chunkedList[2], $chunkedList[3]);
161 1
                break;
162
163
            default:
164
                throw new \RuntimeException('Invalid version');
165 6
        }
166
167 6
        return new LogicalAnd(
168 6
            $lowerVersionConstraint,
169
            $upperVersionConstraint
170 6
        );
171
    }
172
173
    /**
174
     * Chuck the tokens, splitting on hyphen.
175
     *
176
     * @param Token[] $tokenList
177
     *
178
     * @return Token[][]
179
     */
180 11
    private function chunk(array $tokenList)
181
    {
182 11
        $tokenListCount = count($tokenList);
183 11
        $chunkedList = [];
184 11
        $accumulator = [];
185
186 11
        for ($i = 0; $i < $tokenListCount; $i++) {
187 11
            $token = $tokenList[$i];
188
189
            // Accumulate until we hit a dash
190 11
            if (Token::DASH_SEPARATOR !== $token->getType()) {
191 11
                $accumulator[] = $token;
192
1 ignored issue
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
193 11
            } else {
194 10
                $chunkedList[] = $accumulator;
195 10
                $accumulator = [];
196
            }
197 11
        }
198
199 11
        if (count($accumulator)) {
200 11
            $chunkedList[] = $accumulator;
201 11
        }
202
203 11
        return $chunkedList;
204
    }
205
206
    /**
207
     * Determines the correct lower version constraint for a hyphenated range.
208
     *
209
     * @param Token[] $versionTokenList
210
     * @param Token[] $labelTokenList
211
     *
212
     * @return VersionRangeInterface
213
     */
214 6
    private function getLowerConstraint(array $versionTokenList, array $labelTokenList = [])
215
    {
216 6
        return new ComparatorVersion(
217 6
            $this->greaterOrEqualTo,
218 6
            $this->versionBuilder->buildFromTokens($versionTokenList, $labelTokenList)
0 ignored issues
show
Bug introduced by
It seems like $this->versionBuilder->b...nList, $labelTokenList) can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
219 6
        );
220
    }
221
222
    /**
223
     * Determines the correct upper version constraint for a hyphenated range.
224
     *
225
     * @param Token[] $versionTokenList
226
     * @param Token[] $labelTokenList
227
     *
228
     * @return VersionRangeInterface
229
     */
230 6
    private function getUpperConstraint(array $versionTokenList, array $labelTokenList = [])
231
    {
232 6
        $major = 0;
233 6
        $minor = 0;
234 6
        $patch = 0;
235 6
        $label = null;
0 ignored issues
show
Unused Code introduced by
$label is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
236 6
        $comparator = $this->lessThan;
237 6
        $labelBuilder = new LabelBuilder();
238
239 6
        switch (count($versionTokenList)) {
240 6
            case 1:
241 1
                $major = $versionTokenList[0]->getValue() + 1;
242 1
                break;
243
244 5
            case 3:
245 1
                $major = $versionTokenList[0]->getValue();
246 1
                $minor = $versionTokenList[2]->getValue() + 1;
247 1
                break;
248
249 4
            case 5:
250 4
                $comparator = $this->lessOrEqualTo;
251 4
                $major = $versionTokenList[0]->getValue();
252 4
                $minor = $versionTokenList[2]->getValue();
253 4
                $patch = $versionTokenList[4]->getValue();
254 4
                break;
255 6
        }
256
257 6
        return new ComparatorVersion(
258 6
            $comparator,
259 6
            new Version(
260 6
                $major,
261 6
                $minor,
262 6
                $patch,
263 6
                $labelBuilder->buildFromTokens($labelTokenList)
264 6
            )
265 6
        );
266
    }
267
}
268