Completed
Push — master ( 1d4dc2...f35f34 )
by brian
01:54
created

VersionTokenizer::addImplicitAnd()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 2
nop 1
crap 3
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;
13
14
/**
15
 * Tokenizer for version numbers.
16
 */
17
final class VersionTokenizer
18
{
19
    /**
20
     * Accepts a version string & returns an array of tokenized values.
21
     *
22
     * @todo Handle build metadata ('+' at end of version followed by [0-9a-z-]+
23
     *
24
     * @param string $versionString
25
     *
26
     * @return Token[]
27
     */
28 31
    public function tokenize($versionString)
29
    {
30
        /** @var Token[] $tokenList */
31 31
        $tokenList = [];
32 31
        $digitAccumulator = '';
33 31
        $stringAccumulator = '';
34
35
        // Iterate through string, character-by-character
36 31
        for ($i = 0; $i < strlen($versionString); $i++) {
37 31
            $chr = $this->getCharacter($i, $versionString);
38
39 31
            switch (true) {
1 ignored issue
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
40
41
                // Simple token types - separators and range indicators
42 31
                case !is_null($this->getSimpleToken($chr)):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
43
44
                    // Handle preceding digits or labels
45 29
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
46 29
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
47
48 29
                    $tokenList[] = $this->getSimpleToken($chr);
49 29
                    break;
50
51
                // Comparator token types
52 31
                case !is_null($token = $this->getComparatorToken($i, $versionString)):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
53
54
                    // Handle preceding digits or labels
55 11
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
56 11
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
57
58 11
                    $this->addImplicitAnd($tokenList);
59
60 11
                    $tokenList[] = $token;
61 11
                    break;
62
63
                // Skip the 'v' character if immediately preceding a digit
64 31
                case $this->isPrefixedV($i, $versionString):
65
                    // Do nothing
66
                    // TODO: Should we store this for correct reverse transformation?
67 1
                    break;
68
69
                // Spaces, pipes, ampersands and commas may contextually be logical AND/OR
70 31
                case $this->isPossibleLogicalOperator($chr):
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
71
72
                    // Handle preceding digits or labels
73 6
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
74 6
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
75
76
                    $operatorTokenList = [
77 6
                        Token::GREATER_THAN,
78 6
                        Token::GREATER_THAN_EQUAL,
79 6
                        Token::LESS_THAN,
80 6
                        Token::LESS_THAN_EQUAL,
81
                        Token::EQUAL_TO
82 6
                    ];
83
84
                    // No previous tokens, or previous token was not a comparator
85
                    if (
86 6
                        !count($tokenList)
87 6
                        || !in_array($tokenList[count($tokenList)-1]->getType(), $operatorTokenList)
88 6
                    ) {
89 5
                        $possibleOperator = trim($chr);
90
91 5
                        for ($j = $i + 1; $j < strlen($versionString); $j++) {
92 5
                            $operatorChr = $this->getCharacter($j, $versionString);
93
94 5
                            if ($this->isPossibleLogicalOperator($operatorChr)) {
95 3
                                $possibleOperator .= trim($operatorChr);
96 3
                            } else {
97 5
                                if (!strlen($possibleOperator) || ',' === $possibleOperator) {
98 4
                                    $tokenList[] = new Token(Token::LOGICAL_AND, $possibleOperator);
99 4
                                } else {
100 2
                                    $tokenList[] = new Token(Token::LOGICAL_OR, $possibleOperator);
101
                                }
102 5
                                $i = $j - 1;
103 5
                                break;
104
                            }
105 3
                        }
106 5
                    }
107
108 6
                    break;
109
110
                // Start accumulating on the first non-digit & continue until we reach a separator
111 31
                case !is_numeric($chr) || strlen($stringAccumulator):
112 5
                    $stringAccumulator .= $chr;
113 5
                    break;
114
115
                // Simple digits
116 31
                default:
117 31
                    $digitAccumulator .= $chr;
118 31
                    break;
119 31
            }
120 31
        }
121
122
        // Handle any remaining digits or labels
123 31
        $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
124 31
        $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
125
126 31
        return $tokenList;
127
    }
128
129
    /**
130
     * Add an implicit AND between a version & the comparator of a subsequent version.
131
     *
132
     * @param Token[] $tokenList
133
     */
134 11
    private function addImplicitAnd(array &$tokenList)
135
    {
136
        $digitTokenList = [
137 11
            Token::DIGITS,
138
            Token::WILDCARD_DIGITS
139 11
        ];
140
141 11
        if (count($tokenList) && in_array($tokenList[count($tokenList) -1]->getType(), $digitTokenList)) {
142 1
            $tokenList[] = new Token(Token::LOGICAL_AND, '');
143 1
        }
144 11
    }
145
146
    /**
147
     * Returns true if the character is an AND comparator
148
     *
149
     * @param string $chr
150
     *
151
     * @return bool
152
     */
153 31
    private function isPossibleLogicalOperator($chr)
154
    {
155 31
        return in_array($chr, array(',', '|')) || ctype_space($chr);
156
    }
157
158
    /**
159
     * Tries to find a comparator token beginning at the specified index.
160
     *
161
     * @param int $index
162
     * @param string $versionString
163
     *
164
     * @return Token|null
165
     */
166 31
    private function getComparatorToken(&$index, $versionString)
167
    {
168
        $comparatorMap = [
169 31
            '>' => Token::GREATER_THAN,
170 31
            '>=' => Token::GREATER_THAN_EQUAL,
171 31
            '<' => Token::LESS_THAN,
172 31
            '<=' => Token::LESS_THAN_EQUAL,
173
            '=' => Token::EQUAL_TO
174 31
        ];
175
176 31
        $chr = substr($versionString, $index, 1);
177
178 31
        $token = null;
179 31
        if (array_key_exists($chr, $comparatorMap)) {
180 11
            $comparator = $chr;
181
182
            // We have a '<=' or '=>'
183 11
            $nextChr = $this->getCharacter($index + 1, $versionString);
184 11
            if (array_key_exists($chr . $nextChr, $comparatorMap)) {
185 6
                $comparator .= $nextChr;
186 6
                $index++;
187 6
            }
188
189 11
            $token = new Token($comparatorMap[$comparator], $comparator);
190 11
        }
191
192 31
        return $token;
193
    }
194
195
    /**
196
     * Get a token from a single-character.
197
     *
198
     * @param string $chr
199
     *
200
     * @return Token|null
201
     */
202 31
    private function getSimpleToken($chr)
203
    {
204
        $tokenMap = [
205 31
            '-' => Token::DASH_SEPARATOR,
206 31
            '.' => Token::DOT_SEPARATOR,
207 31
            '~' => Token::TILDE_RANGE,
208 31
            '^' => Token::CARET_RANGE,
209 31
            '*' => Token::WILDCARD_DIGITS,
210
            'x' => Token::WILDCARD_DIGITS
211 31
        ];
212
213 31
        $token = null;
214 31
        if (array_key_exists($chr, $tokenMap)) {
215 29
            $token = new Token($tokenMap[$chr], $chr);
216 29
        }
217
218 31
        return $token;
219
    }
220
221
    /**
222
     * Add a token to the list if the string length is greater than 0, empties string.
223
     *
224
     * @param string $type One of Token class constants
225
     * @param string $value
226
     * @param Token[] $tokenList
227
     */
228 31
    public function conditionallyAddToken($type, &$value, &$tokenList)
229
    {
230 31
        if (strlen($value)) {
231 31
            $tokenList[] = new Token(
232 31
                $type,
233
                $value
234 31
            );
235
236 31
            $value = '';
237 31
        }
238 31
    }
239
240
    /**
241
     * Returns true if the character is a 'v' prefix to a version number (e.g. v1.0.7).
242
     *
243
     * @param int $index
244
     * @param string $versionString
245
     *
246
     * @return bool
247
     */
248 31
    private function isPrefixedV($index, $versionString)
249
    {
250 31
        $chr = $this->getCharacter($index, $versionString);
251
252
        return 'v' === $chr
253 31
            && $index + 1 < strlen($versionString)
254 31
            && is_numeric($this->getCharacter($index + 1, $versionString));
255
    }
256
257
    /**
258
     * Get the next character after the specified index or an empty string if not present.
259
     *
260
     * @param int $index
261
     * @param string $versionString
262
     * @return string
263
     */
264 31
    private function getCharacter($index, $versionString)
265
    {
266 31
        $chr = '';
267 31
        if ($index < strlen($versionString)) {
268 31
            $chr = substr($versionString, $index, 1);
269 31
        }
270
271 31
        return $chr;
272
    }
273
}
274