Completed
Push — master ( 8d7014...4fa85a )
by brian
05:27
created

VersionTokenizer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 1

Test Coverage

Coverage 98.81%

Importance

Changes 0
Metric Value
wmc 31
cbo 1
dl 0
loc 257
ccs 83
cts 84
cp 0.9881
rs 9.8
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
D tokenize() 0 100 14
A addImplicitAnd() 0 11 3
A isPossibleLogicalOperator() 0 4 2
B getComparatorToken() 0 28 3
A getSimpleToken() 0 18 2
A conditionallyAddToken() 0 11 2
A isPrefixedV() 0 8 3
A getCharacter() 0 9 2
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 30
    public function tokenize($versionString)
29
    {
30
        /** @var Token[] $tokenList */
31 30
        $tokenList = [];
32 30
        $digitAccumulator = '';
33 30
        $stringAccumulator = '';
34
35
        // Iterate through string, character-by-character
36 30
        for ($i = 0; $i < strlen($versionString); $i++) {
37 30
            $chr = $this->getCharacter($i, $versionString);
38
39
            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 30
                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 28
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
46 28
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
47
48 28
                    $tokenList[] = $this->getSimpleToken($chr);
49 28
                    break;
50
51
                // Comparator token types
52 30
                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 10
                    $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
56 10
                    $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
57
58 10
                    $this->addImplicitAnd($tokenList);
59
60 10
                    $tokenList[] = $token;
61 10
                    break;
62
63
                // Skip the 'v' character if immediately preceding a digit
64 30
                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 30
                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
                        Token::GREATER_THAN_EQUAL,
79
                        Token::LESS_THAN,
80
                        Token::LESS_THAN_EQUAL,
81
                        Token::EQUAL_TO
82
                    ];
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
                    ) {
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
                            } else {
97 5
                                if (!strlen($possibleOperator) || ',' === $possibleOperator) {
98 4
                                    $tokenList[] = new Token(Token::LOGICAL_AND, $possibleOperator);
99
                                } else {
100 2
                                    $tokenList[] = new Token(Token::LOGICAL_OR, $possibleOperator);
101
                                }
102 5
                                $i = $j - 1;
103 5
                                break;
104
                            }
105
                        }
106
                    }
107
108 6
                    break;
109
110
                // Start accumulating on the first non-digit & continue until we reach a separator
111 30
                case !is_numeric($chr) || strlen($stringAccumulator):
112 5
                    $stringAccumulator .= $chr;
113 5
                    break;
114
115
                // Simple digits
116
                default:
117 30
                    $digitAccumulator .= $chr;
118 30
                    break;
119
            }
120
        }
121
122
        // Handle any remaining digits or labels
123 30
        $this->conditionallyAddToken(Token::DIGITS, $digitAccumulator, $tokenList);
124 30
        $this->conditionallyAddToken(Token::LABEL_STRING, $stringAccumulator, $tokenList);
125
126 30
        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 10
    private function addImplicitAnd(array &$tokenList)
135
    {
136
        $digitTokenList = [
137 10
            Token::DIGITS,
138
            Token::WILDCARD_DIGITS
139
        ];
140
141 10
        if (count($tokenList) && in_array($tokenList[count($tokenList) -1]->getType(), $digitTokenList)) {
142
            $tokenList[] = new Token(Token::LOGICAL_AND, '');
143
        }
144 10
    }
145
146
    /**
147
     * Returns true if the character is an AND comparator
148
     *
149
     * @param string $chr
150
     *
151
     * @return bool
152
     */
153 30
    private function isPossibleLogicalOperator($chr)
154
    {
155 30
        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 30
    private function getComparatorToken(&$index, $versionString)
167
    {
168
        $comparatorMap = [
169 30
            '>' => Token::GREATER_THAN,
170
            '>=' => Token::GREATER_THAN_EQUAL,
171
            '<' => Token::LESS_THAN,
172
            '<=' => Token::LESS_THAN_EQUAL,
173
            '=' => Token::EQUAL_TO
174
        ];
175
176 30
        $chr = substr($versionString, $index, 1);
177
178 30
        $token = null;
179 30
        if (array_key_exists($chr, $comparatorMap)) {
180 10
            $comparator = $chr;
181
182
            // We have a '<=' or '=>'
183 10
            $nextChr = $this->getCharacter($index + 1, $versionString);
184 10
            if (array_key_exists($chr . $nextChr, $comparatorMap)) {
185 5
                $comparator .= $nextChr;
186 5
                $index++;
187
            }
188
189 10
            $token = new Token($comparatorMap[$comparator], $comparator);
190
        }
191
192 30
        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 30
    private function getSimpleToken($chr)
203
    {
204
        $tokenMap = [
205 30
            '-' => Token::DASH_SEPARATOR,
206
            '.' => Token::DOT_SEPARATOR,
207
            '~' => Token::TILDE_RANGE,
208
            '^' => Token::CARET_RANGE,
209
            '*' => Token::WILDCARD_DIGITS,
210
            'x' => Token::WILDCARD_DIGITS
211
        ];
212
213 30
        $token = null;
214 30
        if (array_key_exists($chr, $tokenMap)) {
215 28
            $token = new Token($tokenMap[$chr], $chr);
216
        }
217
218 30
        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 30
    public function conditionallyAddToken($type, &$value, &$tokenList)
229
    {
230 30
        if (strlen($value)) {
231 30
            $tokenList[] = new Token(
232
                $type,
233
                $value
234
            );
235
236 30
            $value = '';
237
        }
238 30
    }
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 30
    private function isPrefixedV($index, $versionString)
249
    {
250 30
        $chr = $this->getCharacter($index, $versionString);
251
252 30
        return 'v' === $chr
253 30
            && $index + 1 < strlen($versionString)
254 30
            && 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 30
    private function getCharacter($index, $versionString)
265
    {
266 30
        $chr = '';
267 30
        if ($index < strlen($versionString)) {
268 30
            $chr = substr($versionString, $index, 1);
269
        }
270
271 30
        return $chr;
272
    }
273
}
274