Completed
Push — master ( b1fa0d...41a020 )
by Milos
02:15
created

Parser   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 331
Duplicated Lines 12.99 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 15
dl 43
loc 331
rs 6.6108
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 9 5
A getMode() 0 4 1
A setMode() 0 4 1
A getVersion() 0 4 1
A setVersion() 0 4 1
A parse() 0 17 2
A path() 0 15 3
B disjunction() 22 22 5
B conjunction() 21 21 5
B factor() 0 28 6
A valuePath() 0 11 1
A comparisionExpression() 0 15 2
A attributePath() 0 16 3
A comparisonOperator() 0 9 2
B compareValue() 0 27 3
A isValuePathIncoming() 0 7 2
B match() 0 14 5
B syntaxError() 0 19 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the tmilos/scim-filter-parser package.
5
 *
6
 * (c) Milos Tomic <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Tmilos\ScimFilterParser;
13
14
use Tmilos\Lexer\Config\LexerArrayConfig;
15
use Tmilos\Lexer\Lexer;
16
use Tmilos\Lexer\Token;
17
use Tmilos\ScimFilterParser\Ast\Tokens;
18
19
class Parser
20
{
21
    /** @var Lexer */
22
    private $lexer;
23
24
    /** @var Version */
25
    private $version;
26
27
    /** @var Mode */
28
    private $mode;
29
30
    /** @var bool */
31
    private $inValuePath;
32
33
    /**
34
     * @param Mode    $mode
35
     * @param Version $version
36
     */
37
    public function __construct(Mode $mode = null, Version $version = null)
38
    {
39
        $this->lexer = new Lexer(new LexerArrayConfig(LexerConfigFactory::getConfig()));
40
        $this->version = $version ?: Version::V2();
41
        $this->mode = $mode ?: Mode::FILTER();
42
        if ($this->mode->equals(Mode::PATH) && !$this->version->equals(Version::V2)) {
43
            throw new \InvalidArgumentException('Path mode is available only in SCIM version 2');
44
        }
45
    }
46
47
    /**
48
     * @return Mode
49
     */
50
    public function getMode()
51
    {
52
        return $this->mode;
53
    }
54
55
    /**
56
     * @param Mode $mode
57
     */
58
    public function setMode(Mode $mode)
59
    {
60
        $this->mode = $mode;
61
    }
62
63
    /**
64
     * @return Version
65
     */
66
    public function getVersion()
67
    {
68
        return $this->version;
69
    }
70
71
    /**
72
     * @param Version $version
73
     */
74
    public function setVersion(Version $version)
75
    {
76
        $this->version = $version;
77
    }
78
79
    /**
80
     * @param string $input
81
     *
82
     * @return Ast\Node
83
     */
84
    public function parse($input)
85
    {
86
        $this->inValuePath = false;
87
        $this->lexer->setInput($input);
88
        $this->lexer->moveNext();
89
90
        $node = null;
0 ignored issues
show
Unused Code introduced by
$node 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...
91
        if ($this->mode->equals(Mode::FILTER)) {
92
            $node = $this->disjunction();
93
        } else {
94
            $node = $this->path();
95
        }
96
97
        $this->match(null);
98
99
        return $node;
100
    }
101
102
    /**
103
     * @return Ast\Path
104
     */
105
    private function path()
106
    {
107
        if ($this->isValuePathIncoming()) {
108
            $valuePath = $this->valuePath();
109
            $attributePath = null;
110
            if ($this->lexer->isNextToken(Tokens::T_DOT)) {
111
                $this->match(Tokens::T_DOT);
112
                $attributePath = $this->attributePath();
113
            }
114
115
            return Ast\Path::fromValuePath($valuePath, $attributePath);
116
        }
117
118
        return Ast\Path::fromAttributePath($this->attributePath());
119
    }
120
121
    /**
122
     * @return Ast\Term|Ast\Disjunction
123
     */
124 View Code Duplication
    private function disjunction()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
125
    {
126
        /** @var Ast\Term[] $terms */
127
        $terms = [];
128
        $terms[] = $this->conjunction();
129
130
        if ($this->lexer->isNextToken(Tokens::T_SP)) {
131
            $nextToken = $this->lexer->glimpse();
132
            if ($nextToken && $nextToken->is(Tokens::T_OR)) {
133
                $this->match(Tokens::T_SP);
134
                $this->match(Tokens::T_OR);
135
                $this->match(Tokens::T_SP);
136
                $terms[] = $this->conjunction();
137
            }
138
        }
139
140
        if (count($terms) == 1) {
141
            return $terms[0];
142
        }
143
144
        return new Ast\Disjunction($terms);
0 ignored issues
show
Documentation introduced by
$terms is of type array<integer,object<Tmi...arser\Ast\Disjunction>>, but the function expects a array<integer,object<Tmi...FilterParser\Ast\Term>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
145
    }
146
147
    /**
148
     * @return Ast\Conjunction|Ast\Factor
149
     */
150 View Code Duplication
    private function conjunction()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
151
    {
152
        $factors = [];
153
        $factors[] = $this->factor();
154
155
        if ($this->lexer->isNextToken(Tokens::T_SP)) {
156
            $nextToken = $this->lexer->glimpse();
157
            if ($nextToken && $nextToken->is(Tokens::T_AND)) {
158
                $this->match(Tokens::T_SP);
159
                $this->match(Tokens::T_AND);
160
                $this->match(Tokens::T_SP);
161
                $factors[] = $this->factor();
162
            }
163
        }
164
165
        if (count($factors) == 1) {
166
            return $factors[0];
167
        }
168
169
        return new Ast\Conjunction($factors);
170
    }
171
172
    /**
173
     * @return Ast\Filter
174
     */
175
    private function factor()
176
    {
177
        if ($this->lexer->isNextToken(Tokens::T_NOT)) {
178
            // not ( filter )
179
            $this->match(Tokens::T_NOT);
180
            $this->match(Tokens::T_SP);
181
            $this->match(Tokens::T_PAREN_OPEN);
182
            $filter = $this->disjunction();
183
            $this->match(Tokens::T_PAREN_CLOSE);
184
185
            return new Ast\Negation($filter);
186
        } elseif ($this->lexer->isNextToken(Tokens::T_PAREN_OPEN)) {
187
            // ( filter )
188
            $this->match(Tokens::T_PAREN_OPEN);
189
            $filter = $this->disjunction();
190
            $this->match(Tokens::T_PAREN_CLOSE);
191
192
            return $filter;
193
        }
194
195
        if ($this->version->equals(Version::V2()) && !$this->inValuePath) {
196
            if ($this->isValuePathIncoming()) {
197
                return $this->valuePath();
198
            }
199
        }
200
201
        return $this->comparisionExpression();
202
    }
203
204
    /**
205
     * @return Ast\ValuePath
206
     */
207
    private function valuePath()
208
    {
209
        $attributePath = $this->attributePath();
210
        $this->match(Tokens::T_BRACKET_OPEN);
211
        $this->inValuePath = true;
212
        $filter = $this->disjunction();
213
        $this->match(Tokens::T_BRACKET_CLOSE);
214
        $this->inValuePath = false;
215
216
        return new Ast\ValuePath($attributePath, $filter);
217
    }
218
219
    /**
220
     * @return Ast\ComparisonExpression
221
     */
222
    private function comparisionExpression()
223
    {
224
        $attributePath = $this->attributePath();
225
        $this->match(Tokens::T_SP);
226
227
        $operator = $this->comparisonOperator();
228
229
        $compareValue = null;
230
        if ($operator != 'pr') {
231
            $this->match(Tokens::T_SP);
232
            $compareValue = $this->compareValue();
233
        }
234
235
        return new Ast\ComparisonExpression($attributePath, $operator, $compareValue);
236
    }
237
238
    /**
239
     * @param Ast\AttributePath $attributePath
240
     *
241
     * @return Ast\AttributePath
242
     */
243
    private function attributePath(Ast\AttributePath $attributePath = null)
244
    {
245
        $this->match(Tokens::T_ATTR_NAME);
246
247
        if (!$attributePath) {
248
            $attributePath = new Ast\AttributePath();
249
        }
250
        $attributePath->add($this->lexer->getToken()->getValue());
251
252
        if ($this->lexer->isNextToken(Tokens::T_DOT)) {
253
            $this->match(Tokens::T_DOT);
254
            $this->attributePath($attributePath);
255
        }
256
257
        return $attributePath;
258
    }
259
260
    /**
261
     * @return string
262
     */
263
    private function comparisonOperator()
264
    {
265
        if (!$this->lexer->isNextTokenAny(Tokens::compareOperators())) {
266
            $this->syntaxError('comparision operator');
267
        }
268
        $this->match($this->lexer->getLookahead()->getName());
269
270
        return $this->lexer->getToken()->getValue();
271
    }
272
273
    /**
274
     * @return mixed
275
     */
276
    private function compareValue()
277
    {
278
        if (!$this->lexer->isNextTokenAny(Tokens::compareValues())) {
279
            $this->syntaxError('comparison value');
280
        }
281
        $this->match($this->lexer->getLookahead()->getName());
282
283
        $value = json_decode($this->lexer->getToken()->getValue());
284
        if (preg_match(
285
                '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D',
286
                $value,
287
                $matches
288
            )) {
289
            $year = intval($matches[1]);
290
            $month = intval($matches[2]);
291
            $day = intval($matches[3]);
292
            $hour = intval($matches[4]);
293
            $minute = intval($matches[5]);
294
            $second = intval($matches[6]);
295
            // Use gmmktime because the timestamp will always be given in UTC.
296
            $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
297
298
            $value = new \DateTime('@'.$ts, new \DateTimeZone('UTC'));
299
        }
300
301
        return $value;
302
    }
303
304
    /**
305
     * @return bool
306
     */
307
    private function isValuePathIncoming()
308
    {
309
        $tokenAfterAttributePath = $this->lexer->peekWhileTokens([Tokens::T_ATTR_NAME, Tokens::T_DOT]);
310
        $this->lexer->resetPeek();
311
312
        return $tokenAfterAttributePath ? $tokenAfterAttributePath->is(Tokens::T_BRACKET_OPEN) : false;
313
    }
314
315
    private function match($tokenName)
316
    {
317
        if (null === $tokenName) {
318
            if ($this->lexer->getLookahead()) {
319
                $this->syntaxError('end of input');
320
            }
321
        } else {
322
            if (!$this->lexer->getLookahead() || !$this->lexer->getLookahead()->is($tokenName)) {
323
                $this->syntaxError($tokenName);
324
            }
325
326
            $this->lexer->moveNext();
327
        }
328
    }
329
330
    private function syntaxError($expected = '', Token $token = null)
331
    {
332
        if (null === $token) {
333
            $token = $this->lexer->getLookahead();
334
        }
335
        if ($token) {
336
            $offset = $token->getOffset();
337
        } elseif ($this->lexer->getToken()) {
338
            $offset = $this->lexer->getToken()->getOffset();
339
        } else {
340
            $offset = strlen($this->lexer->getInput());
341
        }
342
343
        $message = "line 0, col {$offset}: Error: ";
344
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
345
        $message .= ($token === null) ? 'end of string.' : "'{$token->getValue()}'";
346
347
        throw Error\FilterException::syntaxError($message, Error\FilterException::filterError($this->lexer->getInput()));
348
    }
349
}
350