Completed
Push — master ( c87449...b1fa0d )
by Milos
01:47
created

Parser::isValuePathIncoming()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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 Version
49
     */
50
    public function getVersion()
51
    {
52
        return $this->version;
53
    }
54
55
    /**
56
     * @param Version $version
57
     */
58
    public function setVersion(Version $version)
59
    {
60
        $this->version = $version;
61
    }
62
63
    /**
64
     * @param string $input
65
     *
66
     * @return Ast\Node
67
     */
68
    public function parse($input)
69
    {
70
        $this->inValuePath = false;
71
        $this->lexer->setInput($input);
72
        $this->lexer->moveNext();
73
74
        $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...
75
        if ($this->mode->equals(Mode::FILTER)) {
76
            $node = $this->disjunction();
77
        } else {
78
            $node = $this->path();
79
        }
80
81
        $this->match(null);
82
83
        return $node;
84
    }
85
86
    /**
87
     * @return Ast\Path
88
     */
89
    private function path()
90
    {
91
        if ($this->isValuePathIncoming()) {
92
            $valuePath = $this->valuePath();
93
            $attributePath = null;
94
            if ($this->lexer->isNextToken(Tokens::T_DOT)) {
95
                $this->match(Tokens::T_DOT);
96
                $attributePath = $this->attributePath();
97
            }
98
99
            return Ast\Path::fromValuePath($valuePath, $attributePath);
100
        }
101
102
        return Ast\Path::fromAttributePath($this->attributePath());
103
    }
104
105
    /**
106
     * @return Ast\Term|Ast\Disjunction
107
     */
108 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...
109
    {
110
        /** @var Ast\Term[] $terms */
111
        $terms = [];
112
        $terms[] = $this->conjunction();
113
114
        if ($this->lexer->isNextToken(Tokens::T_SP)) {
115
            $nextToken = $this->lexer->glimpse();
116
            if ($nextToken && $nextToken->is(Tokens::T_OR)) {
117
                $this->match(Tokens::T_SP);
118
                $this->match(Tokens::T_OR);
119
                $this->match(Tokens::T_SP);
120
                $terms[] = $this->conjunction();
121
            }
122
        }
123
124
        if (count($terms) == 1) {
125
            return $terms[0];
126
        }
127
128
        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...
129
    }
130
131
    /**
132
     * @return Ast\Conjunction|Ast\Factor
133
     */
134 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...
135
    {
136
        $factors = [];
137
        $factors[] = $this->factor();
138
139
        if ($this->lexer->isNextToken(Tokens::T_SP)) {
140
            $nextToken = $this->lexer->glimpse();
141
            if ($nextToken && $nextToken->is(Tokens::T_AND)) {
142
                $this->match(Tokens::T_SP);
143
                $this->match(Tokens::T_AND);
144
                $this->match(Tokens::T_SP);
145
                $factors[] = $this->factor();
146
            }
147
        }
148
149
        if (count($factors) == 1) {
150
            return $factors[0];
151
        }
152
153
        return new Ast\Conjunction($factors);
154
    }
155
156
    /**
157
     * @return Ast\Filter
158
     */
159
    private function factor()
160
    {
161
        if ($this->lexer->isNextToken(Tokens::T_NOT)) {
162
            // not ( filter )
163
            $this->match(Tokens::T_NOT);
164
            $this->match(Tokens::T_SP);
165
            $this->match(Tokens::T_PAREN_OPEN);
166
            $filter = $this->disjunction();
167
            $this->match(Tokens::T_PAREN_CLOSE);
168
169
            return new Ast\Negation($filter);
170
        } elseif ($this->lexer->isNextToken(Tokens::T_PAREN_OPEN)) {
171
            // ( filter )
172
            $this->match(Tokens::T_PAREN_OPEN);
173
            $filter = $this->disjunction();
174
            $this->match(Tokens::T_PAREN_CLOSE);
175
176
            return $filter;
177
        }
178
179
        if ($this->version->equals(Version::V2()) && !$this->inValuePath) {
180
            if ($this->isValuePathIncoming()) {
181
                return $this->valuePath();
182
            }
183
        }
184
185
        return $this->comparisionExpression();
186
    }
187
188
    /**
189
     * @return Ast\ValuePath
190
     */
191
    private function valuePath()
192
    {
193
        $attributePath = $this->attributePath();
194
        $this->match(Tokens::T_BRACKET_OPEN);
195
        $this->inValuePath = true;
196
        $filter = $this->disjunction();
197
        $this->match(Tokens::T_BRACKET_CLOSE);
198
        $this->inValuePath = false;
199
200
        return new Ast\ValuePath($attributePath, $filter);
201
    }
202
203
    /**
204
     * @return Ast\ComparisonExpression
205
     */
206
    private function comparisionExpression()
207
    {
208
        $attributePath = $this->attributePath();
209
        $this->match(Tokens::T_SP);
210
211
        $operator = $this->comparisonOperator();
212
213
        $compareValue = null;
214
        if ($operator != 'pr') {
215
            $this->match(Tokens::T_SP);
216
            $compareValue = $this->compareValue();
217
        }
218
219
        return new Ast\ComparisonExpression($attributePath, $operator, $compareValue);
220
    }
221
222
    /**
223
     * @param Ast\AttributePath $attributePath
224
     *
225
     * @return Ast\AttributePath
226
     */
227
    private function attributePath(Ast\AttributePath $attributePath = null)
228
    {
229
        $this->match(Tokens::T_ATTR_NAME);
230
231
        if (!$attributePath) {
232
            $attributePath = new Ast\AttributePath();
233
        }
234
        $attributePath->add($this->lexer->getToken()->getValue());
235
236
        if ($this->lexer->isNextToken(Tokens::T_DOT)) {
237
            $this->match(Tokens::T_DOT);
238
            $this->attributePath($attributePath);
239
        }
240
241
        return $attributePath;
242
    }
243
244
    /**
245
     * @return string
246
     */
247
    private function comparisonOperator()
248
    {
249
        if (!$this->lexer->isNextTokenAny(Tokens::compareOperators())) {
250
            $this->syntaxError('comparision operator');
251
        }
252
        $this->match($this->lexer->getLookahead()->getName());
253
254
        return $this->lexer->getToken()->getValue();
255
    }
256
257
    /**
258
     * @return mixed
259
     */
260
    private function compareValue()
261
    {
262
        if (!$this->lexer->isNextTokenAny(Tokens::compareValues())) {
263
            $this->syntaxError('comparison value');
264
        }
265
        $this->match($this->lexer->getLookahead()->getName());
266
267
        $value = json_decode($this->lexer->getToken()->getValue());
268
        if (preg_match(
269
                '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D',
270
                $value,
271
                $matches
272
            )) {
273
            $year = intval($matches[1]);
274
            $month = intval($matches[2]);
275
            $day = intval($matches[3]);
276
            $hour = intval($matches[4]);
277
            $minute = intval($matches[5]);
278
            $second = intval($matches[6]);
279
            // Use gmmktime because the timestamp will always be given in UTC.
280
            $ts = gmmktime($hour, $minute, $second, $month, $day, $year);
281
282
            $value = new \DateTime('@'.$ts, new \DateTimeZone('UTC'));
283
        }
284
285
        return $value;
286
    }
287
288
    /**
289
     * @return bool
290
     */
291
    private function isValuePathIncoming()
292
    {
293
        $tokenAfterAttributePath = $this->lexer->peekWhileTokens([Tokens::T_ATTR_NAME, Tokens::T_DOT]);
294
        $this->lexer->resetPeek();
295
296
        return $tokenAfterAttributePath ? $tokenAfterAttributePath->is(Tokens::T_BRACKET_OPEN) : false;
297
    }
298
299
    private function match($tokenName)
300
    {
301
        if (null === $tokenName) {
302
            if ($this->lexer->getLookahead()) {
303
                $this->syntaxError('end of input');
304
            }
305
        } else {
306
            if (!$this->lexer->getLookahead() || !$this->lexer->getLookahead()->is($tokenName)) {
307
                $this->syntaxError($tokenName);
308
            }
309
310
            $this->lexer->moveNext();
311
        }
312
    }
313
314
    private function syntaxError($expected = '', Token $token = null)
315
    {
316
        if (null === $token) {
317
            $token = $this->lexer->getLookahead();
318
        }
319
        if ($token) {
320
            $offset = $token->getOffset();
321
        } elseif ($this->lexer->getToken()) {
322
            $offset = $this->lexer->getToken()->getOffset();
323
        } else {
324
            $offset = strlen($this->lexer->getInput());
325
        }
326
327
        $message = "line 0, col {$offset}: Error: ";
328
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
329
        $message .= ($token === null) ? 'end of string.' : "'{$token->getValue()}'";
330
331
        throw Error\FilterException::syntaxError($message, Error\FilterException::filterError($this->lexer->getInput()));
332
    }
333
}
334