Completed
Push — master ( 4aee02...cfd9ba )
by Milos
08:23
created

Parser::isName()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 6
eloc 11
nc 6
nop 2
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
        $colonPos = strrpos($string, ':');
269
        if ($colonPos !== false) {
270
            $schema = substr($string, 0, $colonPos);
271
            $path = substr($string, $colonPos + 1);
272
        } else {
273
            $schema = null;
274
            $path = $string;
275
        }
276
277
        $parts = explode('.', $path);
278
        $attributePath = new Ast\AttributePath();
279
        $attributePath->schema = $schema;
280
        foreach ($parts as $part) {
281
            $attributePath->add($part);
282
        }
283
284
        return $attributePath;
285
    }
286
287
    /**
288
     * @return string
289
     */
290
    private function comparisonOperator()
291
    {
292
        if (!$this->isName(['pr', 'eq', 'ne', 'co', 'sw', 'ew', 'gt', 'lt', 'ge', 'le'], $this->lexer->getLookahead())) {
293
            $this->syntaxError('comparision operator');
294
        }
295
296
        $this->match($this->lexer->getLookahead()->getName());
297
298
        return $this->lexer->getToken()->getValue();
299
    }
300
301
    /**
302
     * @return mixed
303
     */
304
    private function compareValue()
305
    {
306
        if (!$this->lexer->isNextTokenAny([Tokens::T_NAME, Tokens::T_NUMBER, Tokens::T_STRING])) {
307
            $this->syntaxError('comparison value');
308
        }
309
        if ($this->lexer->getLookahead()->is(Tokens::T_NAME) && !$this->isName(['true', 'false', 'null'], $this->lexer->getLookahead())) {
310
            $this->syntaxError('comparision value');
311
        }
312
313
        $this->match($this->lexer->getLookahead()->getName());
314
315
        $value = json_decode($this->lexer->getToken()->getValue());
316
        if (preg_match(
317
                '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D',
318
                $value,
319
                $matches
320
            )) {
321
            $year = intval($matches[1]);
322
            $month = intval($matches[2]);
323
            $day = intval($matches[3]);
324
            $hour = intval($matches[4]);
325
            $minute = intval($matches[5]);
326
            $second = intval($matches[6]);
327
            // Use gmmktime because the timestamp will always be given in UTC.
328
            $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
329
330
            $value = new \DateTime('@'.$ts, new \DateTimeZone('UTC'));
331
        }
332
333
        return $value;
334
    }
335
336
    /**
337
     * @return bool
338
     */
339
    private function isValuePathIncoming()
340
    {
341
        $tokenAfterAttributePath = $this->lexer->peekWhileTokens([Tokens::T_NAME, Tokens::T_DOT]);
342
        $this->lexer->resetPeek();
343
344
        return $tokenAfterAttributePath ? $tokenAfterAttributePath->is(Tokens::T_BRACKET_OPEN) : false;
345
    }
346
347
    /**
348
     * @param string|string[] $value
349
     * @param Token|null      $token
350
     *
351
     * @return bool
352
     */
353
    private function isName($value, $token)
354
    {
355
        if (!$token) {
356
            return false;
357
        }
358
        if (!$token->is(Tokens::T_NAME)) {
359
            return false;
360
        }
361
362
        if (is_array($value)) {
363
            foreach ($value as $v) {
364
                if (strcasecmp($token->getValue(), $v) === 0) {
365
                    return true;
366
                }
367
            }
368
369
            return false;
370
        }
371
372
        return strcasecmp($token->getValue(), $value) === 0;
373
    }
374
375
    private function match($tokenName)
376
    {
377
        if (null === $tokenName) {
378
            if ($this->lexer->getLookahead()) {
379
                $this->syntaxError('end of input');
380
            }
381
        } else {
382
            if (!$this->lexer->getLookahead() || !$this->lexer->getLookahead()->is($tokenName)) {
383
                $this->syntaxError($tokenName);
384
            }
385
386
            $this->lexer->moveNext();
387
        }
388
    }
389
390
    private function syntaxError($expected = '', Token $token = null)
391
    {
392
        if (null === $token) {
393
            $token = $this->lexer->getLookahead();
394
        }
395
        if ($token) {
396
            $offset = $token->getOffset();
397
        } elseif ($this->lexer->getToken()) {
398
            $offset = $this->lexer->getToken()->getOffset();
399
        } else {
400
            $offset = strlen($this->lexer->getInput());
401
        }
402
403
        $message = "line 0, col {$offset}: Error: ";
404
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
405
        $message .= ($token === null) ? 'end of string.' : "'{$token->getValue()}'";
406
407
        throw Error\FilterException::syntaxError($message, Error\FilterException::filterError($this->lexer->getInput()));
408
    }
409
}
410