Completed
Push — master ( cfd9ba...ce4958 )
by Milos
26:38
created

Parser::attributePath()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
cc 6
eloc 19
nc 6
nop 0
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 ($this->isName('or', $nextToken)) {
133
                $this->match(Tokens::T_SP);
134
                $this->match(Tokens::T_NAME);
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 ($this->isName('and', $nextToken)) {
158
                $this->match(Tokens::T_SP);
159
                $this->match(Tokens::T_NAME);
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->isName('not', $this->lexer->getLookahead())) {
178
            // not ( filter )
179
            $this->match(Tokens::T_NAME);
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
     * @return Ast\AttributePath
240
     */
241
    private function attributePath()
242
    {
243
        $string = '';
244
        $valid = [Tokens::T_NUMBER, Tokens::T_NAME, Tokens::T_COLON, Tokens::T_SLASH, Tokens::T_DOT];
245
        $stopping = [Tokens::T_SP, Tokens::T_BRACKET_OPEN];
246
247
        while (true) {
248
            $token = $this->lexer->getLookahead();
249
            if (!$token) {
250
                break;
251
            }
252
            $isValid = in_array($token->getName(), $valid);
253
            $isStopping = in_array($token->getName(), $stopping);
254
            if ($isStopping) {
255
                break;
256
            }
257
            if (!$isValid) {
258
                $this->syntaxError('attribute path');
259
            }
260
            $string .= $token->getValue();
261
            $this->lexer->moveNext();
262
        }
263
264
        if (!$string) {
265
            $this->syntaxError('attribute path');
266
        }
267
268
        return Ast\AttributePath::fromString($string);
269
    }
270
271
    /**
272
     * @return string
273
     */
274
    private function comparisonOperator()
275
    {
276
        if (!$this->isName(['pr', 'eq', 'ne', 'co', 'sw', 'ew', 'gt', 'lt', 'ge', 'le'], $this->lexer->getLookahead())) {
277
            $this->syntaxError('comparision operator');
278
        }
279
280
        $this->match($this->lexer->getLookahead()->getName());
281
282
        return $this->lexer->getToken()->getValue();
283
    }
284
285
    /**
286
     * @return mixed
287
     */
288
    private function compareValue()
289
    {
290
        if (!$this->lexer->isNextTokenAny([Tokens::T_NAME, Tokens::T_NUMBER, Tokens::T_STRING])) {
291
            $this->syntaxError('comparison value');
292
        }
293
        if ($this->lexer->getLookahead()->is(Tokens::T_NAME) && !$this->isName(['true', 'false', 'null'], $this->lexer->getLookahead())) {
294
            $this->syntaxError('comparision value');
295
        }
296
297
        $this->match($this->lexer->getLookahead()->getName());
298
299
        $value = json_decode($this->lexer->getToken()->getValue());
300
        if (preg_match(
301
                '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D',
302
                $value,
303
                $matches
304
            )) {
305
            $year = intval($matches[1]);
306
            $month = intval($matches[2]);
307
            $day = intval($matches[3]);
308
            $hour = intval($matches[4]);
309
            $minute = intval($matches[5]);
310
            $second = intval($matches[6]);
311
            // Use gmmktime because the timestamp will always be given in UTC.
312
            $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
313
314
            $value = new \DateTime('@'.$ts, new \DateTimeZone('UTC'));
315
        }
316
317
        return $value;
318
    }
319
320
    /**
321
     * @return bool
322
     */
323
    private function isValuePathIncoming()
324
    {
325
        $tokenAfterAttributePath = $this->lexer->peekWhileTokens([Tokens::T_NAME, Tokens::T_DOT]);
326
        $this->lexer->resetPeek();
327
328
        return $tokenAfterAttributePath ? $tokenAfterAttributePath->is(Tokens::T_BRACKET_OPEN) : false;
329
    }
330
331
    /**
332
     * @param string|string[] $value
333
     * @param Token|null      $token
334
     *
335
     * @return bool
336
     */
337
    private function isName($value, $token)
338
    {
339
        if (!$token) {
340
            return false;
341
        }
342
        if (!$token->is(Tokens::T_NAME)) {
343
            return false;
344
        }
345
346
        if (is_array($value)) {
347
            foreach ($value as $v) {
348
                if (strcasecmp($token->getValue(), $v) === 0) {
349
                    return true;
350
                }
351
            }
352
353
            return false;
354
        }
355
356
        return strcasecmp($token->getValue(), $value) === 0;
357
    }
358
359
    private function match($tokenName)
360
    {
361
        if (null === $tokenName) {
362
            if ($this->lexer->getLookahead()) {
363
                $this->syntaxError('end of input');
364
            }
365
        } else {
366
            if (!$this->lexer->getLookahead() || !$this->lexer->getLookahead()->is($tokenName)) {
367
                $this->syntaxError($tokenName);
368
            }
369
370
            $this->lexer->moveNext();
371
        }
372
    }
373
374
    private function syntaxError($expected = '', Token $token = null)
375
    {
376
        if (null === $token) {
377
            $token = $this->lexer->getLookahead();
378
        }
379
        if ($token) {
380
            $offset = $token->getOffset();
381
        } elseif ($this->lexer->getToken()) {
382
            $offset = $this->lexer->getToken()->getOffset();
383
        } else {
384
            $offset = strlen($this->lexer->getInput());
385
        }
386
387
        $message = "line 0, col {$offset}: Error: ";
388
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
389
        $message .= ($token === null) ? 'end of string.' : "'{$token->getValue()}'";
390
391
        throw Error\FilterException::syntaxError($message, Error\FilterException::filterError($this->lexer->getInput()));
392
    }
393
}
394