Completed
Push — master ( 249ad3...86526a )
by Milos
01:45
created

Parser::match()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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