AbstractParser::parseComparison()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
nc 6
nop 2
dl 0
loc 32
rs 8.9617
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cloudstek\SCIM\FilterParser;
6
7
use Cloudstek\SCIM\FilterParser\Exception\UnexpectedValueException;
8
use Nette\Tokenizer;
9
10
/**
11
 * Abstract parser.
12
 */
13
abstract class AbstractParser
14
{
15
    protected const T_NUMBER = 10;
16
    protected const T_STRING = 11;
17
    protected const T_BOOL = 12;
18
    protected const T_NULL = 13;
19
20
    protected const T_PAREN_OPEN = 21;
21
    protected const T_PAREN_CLOSE = 22;
22
    protected const T_BRACKET_OPEN = 23;
23
    protected const T_BRACKET_CLOSE = 24;
24
25
    protected const T_NEGATION = 30;
26
    protected const T_LOG_OP = 31;
27
    protected const T_COMP_OP = 32;
28
29
    protected const T_NAME = 40;
30
    protected const T_SUBATTR = 41;
31
32
    protected Tokenizer\Tokenizer $tokenizer;
33
34
    protected ParserMode $mode;
1 ignored issue
show
Bug introduced by
The type Cloudstek\SCIM\FilterParser\ParserMode was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
35
36
    /**
37
     * Abstract parser.
38
     *
39
     * @param ParserMode $mode Parser mode.
40
     */
41
    public function __construct(ParserMode $mode)
42
    {
43
        $patterns = [
44
            self::T_NUMBER => '\d+(?:\.\d+)?',
45
            self::T_STRING => '\"[^\"]*\"',
46
            self::T_BOOL => 'true|false',
47
            self::T_NULL => 'null',
48
            self::T_PAREN_OPEN => '\(',
49
            self::T_PAREN_CLOSE => '\)',
50
            self::T_BRACKET_OPEN => '\[',
51
            self::T_BRACKET_CLOSE => '\]',
52
            self::T_NEGATION => 'not\s+',
53
            self::T_LOG_OP => '\s+(?:and|or)\s+',
54
            self::T_COMP_OP => '\s(?:(?:eq|ne|co|sw|ew|gt|lt|ge|le)\s+|pr)',
55
            self::T_NAME => '(?:(?:[^\"]+\:)+)?[\-\_a-z0-9]+(?:\.[\-\_a-z0-9]+)?',
56
        ];
57
58
        if ($mode === ParserMode::PATH) {
59
            $patterns[self::T_SUBATTR] = '\.[\-\_a-z0-9]+$';
60
        }
61
62
        // Sort patterns by key as constant value determines order.
63
        ksort($patterns);
64
65
        $this->tokenizer = new Tokenizer\Tokenizer(
66
            $patterns,
67
            'i'
68
        );
69
70
        $this->mode = $mode;
71
    }
72
73
    /**
74
     * Parse.
75
     *
76
     * @param string $input
77
     *
78
     * @return AST\Node|AST\Path|null
79
     */
80
    abstract public function parse(string $input);
81
82
    /**
83
     * Parse inner.
84
     *
85
     * @param Tokenizer\Stream $stream
86
     * @param bool             $inValuePath
87
     *
88
     * @throws Tokenizer\Exception
89
     *
90
     * @return AST\Node|null
91
     */
92
    protected function parseInner(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
93
    {
94
        $node = null;
95
96
        if ($stream->isNext(self::T_PAREN_OPEN)) {
97
            // Parentheses
98
            $node = $this->parseParentheses($stream);
99
100
            if ($node === null) {
101
                return null;
102
            }
103
        } elseif ($stream->isNext(self::T_NEGATION)) {
104
            // Negation
105
            $node = $this->parseNegation($stream, $inValuePath);
106
107
            if ($node === null) {
108
                return null;
109
            }
110
        } elseif ($stream->isNext(self::T_NAME)) {
111
            // Comparison or value path
112
            $node = $this->parseAttributePath($stream, $inValuePath);
113
114
            if ($node instanceof AST\AttributePath) {
115
                $node = $this->parseComparison($stream, $node);
116
            }
117
        }
118
119
        // Make sure we only return nodes, not paths.
120
        if ($node !== null && $node instanceof AST\Node) {
121
            // Logical connective
122
            if ($stream->isNext(self::T_LOG_OP)) {
123
                return $this->parseConnective($stream, $node, $inValuePath);
124
            }
125
126
            return $node;
127
        }
128
129
        throw new UnexpectedValueException($stream);
130
    }
131
132
    /**
133
     * Parse filter in parentheses.
134
     *
135
     * @param Tokenizer\Stream $stream
136
     *
137
     * @throws Tokenizer\Exception
138
     *
139
     * @return AST\Node|AST\Path|null
140
     */
141
    protected function parseParentheses(Tokenizer\Stream $stream)
142
    {
143
        $stream->consumeToken(self::T_PAREN_OPEN);
144
        $filter = $this->joinUntilMatchingParenthesis($stream);
145
        $stream->consumeToken(self::T_PAREN_CLOSE);
146
147
        if (empty($filter)) {
148
            return null;
149
        }
150
151
        return $this->parse($filter);
152
    }
153
154
    /**
155
     * Parse negation.
156
     *
157
     * @param Tokenizer\Stream $stream
158
     * @param bool             $inValuePath
159
     *
160
     * @throws Tokenizer\Exception
161
     *
162
     * @return AST\Node|null
163
     */
164
    protected function parseNegation(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
165
    {
166
        $stream->consumeToken(self::T_NEGATION);
167
        $stream->consumeToken(self::T_PAREN_OPEN);
168
        $filter = $this->joinUntilMatchingParenthesis($stream);
169
        $stream->consumeToken(self::T_PAREN_CLOSE);
170
171
        if (empty($filter)) {
172
            return null;
173
        }
174
175
        $node = $this->parseInner($this->tokenizer->tokenize($filter), $inValuePath);
176
177
        if ($node === null) {
178
            return null;
179
        }
180
181
        return new AST\Negation($node);
182
    }
183
184
    /**
185
     * Parse attribute path.
186
     *
187
     * @param Tokenizer\Stream $stream
188
     * @param bool             $inValuePath
189
     *
190
     * @throws Tokenizer\Exception
191
     *
192
     * @return AST\Path
193
     */
194
    protected function parseAttributePath(Tokenizer\Stream $stream, bool $inValuePath = false): AST\Path
195
    {
196
        $name = $stream->consumeToken(self::T_NAME)->value;
197
198
        // Attribute scheme
199
        $scheme = null;
200
201
        if (strpos($name, ':') !== false) {
202
            /** @var int $lastColonPos */
203
            $lastColonPos = strrpos($name, ':');
204
            $scheme = substr($name, 0, $lastColonPos);
205
            $name = substr($name, $lastColonPos + 1);
206
        }
207
208
        // Attribute path
209
        $attributePath = new AST\AttributePath($scheme, explode('.', $name));
210
211
        // Value path (only if not in value path already
212
        if ($stream->isNext(self::T_BRACKET_OPEN)) {
213
            if ($inValuePath === true || count($attributePath) !== 1) {
214
                throw new UnexpectedValueException($stream);
215
            }
216
217
            return $this->parseValuePath($stream, $attributePath);
218
        }
219
220
        return $attributePath;
221
    }
222
223
    /**
224
     * Parse value path.
225
     *
226
     * @param Tokenizer\Stream  $stream
227
     * @param AST\AttributePath $attributePath
228
     *
229
     * @throws Tokenizer\Exception
230
     *
231
     * @return AST\ValuePath
232
     */
233
    protected function parseValuePath(Tokenizer\Stream $stream, AST\AttributePath $attributePath): AST\ValuePath
234
    {
235
        // Save position to report exception coordinates later
236
        $startPos = $stream->position + 1;
237
238
        // Parse
239
        $stream->consumeToken(self::T_BRACKET_OPEN);
240
        $node = $this->parseInner($stream, true);
241
        $stream->consumeToken(self::T_BRACKET_CLOSE);
242
243
        // Correct attribute path for node.
244
        try {
245
            $node = $this->updateValuePathAttributePath($attributePath, $node);
246
        } catch (Tokenizer\Exception $ex) {
247
            // Reset stream position to value path start to correct exception coordinates.
248
            $stream->position = $startPos;
249
250
            throw new UnexpectedValueException($stream);
251
        }
252
253
        return new AST\ValuePath($attributePath, $node);
254
    }
255
256
    /**
257
     * Parse comparison.
258
     *
259
     * @param Tokenizer\Stream  $stream
260
     * @param AST\AttributePath $attributePath
261
     *
262
     * @throws Tokenizer\Exception
263
     *
264
     * @return AST\Connective|AST\Comparison
265
     */
266
    protected function parseComparison(
267
        Tokenizer\Stream $stream,
268
        AST\AttributePath $attributePath
269
    ) {
270
        $operator = trim($stream->consumeValue(self::T_COMP_OP));
271
        $value = null;
272
273
        if (strcasecmp($operator, 'pr') <> 0) {
274
            $value = $stream->consumeToken(self::T_STRING, self::T_NUMBER, self::T_BOOL, self::T_NULL);
275
276
            switch ($value->type) {
277
                case self::T_STRING:
278
                    $value = trim($value->value, '"');
279
                    break;
280
                case self::T_NUMBER:
281
                    if (strpos($value->value, '.') !== false) {
282
                        $value = floatval($value->value);
283
                        break;
284
                    }
285
286
                    $value = intval($value->value);
287
                    break;
288
                case self::T_BOOL:
289
                    $value = strcasecmp($value->value, 'true') === 0;
290
                    break;
291
                default:
292
                    $value = null;
293
                    break;
294
            }
295
        }
296
297
        return new AST\Comparison($attributePath, $operator, $value);
298
    }
299
300
    /**
301
     * Parse logical connective (and/or).
302
     *
303
     * @param Tokenizer\Stream $stream
304
     * @param AST\Node         $leftNode
305
     * @param bool             $inValuePath
306
     *
307
     * @throws Tokenizer\Exception
308
     *
309
     * @return AST\Connective
310
     */
311
    protected function parseConnective(
312
        Tokenizer\Stream $stream,
313
        AST\Node $leftNode,
314
        bool $inValuePath = false
315
    ): AST\Connective {
316
        // Logical operator
317
        $logOp = trim($stream->consumeToken(self::T_LOG_OP)->value);
318
319
        $isConjunction = strcasecmp($logOp, 'and') === 0;
320
321
        // Save position to report exception coordinates later
322
        $startPos = $stream->position + 1;
323
324
        // Parse right hand node
325
        $rightNode = $this->parseInner($stream, $inValuePath);
326
327
        if ($rightNode === null) {
328
            // Reset stream position to value path start to correct exception coordinates.
329
            $stream->position = $startPos;
330
331
            throw new UnexpectedValueException($stream);
332
        }
333
334
        // Connective nodes
335
        $nodes = [$leftNode, $rightNode];
336
337
        // Merge consecutive connectives of the same type into one connective.
338
        if (
339
            ($rightNode instanceof AST\Conjunction && $isConjunction === true)
340
            || ($rightNode instanceof AST\Disjunction && $isConjunction === false)
341
        ) {
342
            $rightNodes = $rightNode->getNodes();
343
344
            $nodes = array_merge([$leftNode], $rightNodes);
345
        }
346
347
        return $isConjunction === true
348
            ? new AST\Conjunction($nodes)
349
            : new AST\Disjunction($nodes);
350
    }
351
352
    /**
353
     * Prepend attribute path to node.
354
     *
355
     * @param AST\AttributePath $attributePath
356
     * @param AST\Node|null     $node
357
     *
358
     * @throws Tokenizer\Exception When node is null or not a valid node.
359
     *
360
     * @return AST\Node
361
     */
362
    protected function updateValuePathAttributePath(AST\AttributePath $attributePath, ?AST\Node $node): AST\Node
363
    {
364
        if ($node instanceof AST\Comparison) {
365
            $names = array_merge($attributePath->getNames(), $node->getAttributePath()->getNames());
366
367
            $node->setAttributePath(
368
                new AST\AttributePath(
369
                    $attributePath->getSchema(),
370
                    $names
371
                )
372
            );
373
374
            return $node;
375
        }
376
377
        if ($node instanceof AST\Negation) {
378
            $node->setNode($this->updateValuePathAttributePath($attributePath, $node->getNode()));
379
380
            return $node;
381
        }
382
383
        if ($node instanceof AST\Connective) {
384
            foreach ($node->getNodes() as $subNode) {
385
                $this->updateValuePathAttributePath($attributePath, $subNode);
386
            }
387
388
            return $node;
389
        }
390
391
        throw new Tokenizer\Exception('Invalid value path.');
392
    }
393
394
    /**
395
     * Join values until matching
396
     *
397
     * @param Tokenizer\Stream $stream
398
     *
399
     * @throws Tokenizer\Exception
400
     * @return string
401
     */
402
    protected function joinUntilMatchingParenthesis(Tokenizer\Stream $stream): string
403
    {
404
        $pos = $stream->position + 1;
405
        $numTokens = count($stream->tokens);
406
        $level = 1;
407
        $result = '';
408
409
        if ($pos >= $numTokens) {
410
            throw new Tokenizer\Exception('Unexpected end of string');
411
        }
412
413
        for ($i = $pos; $pos < $numTokens; $i++) {
414
            $token = $stream->tokens[$i];
415
416
            if ($token->type === self::T_PAREN_OPEN) {
417
                $level++;
418
            } elseif ($token->type === self::T_PAREN_CLOSE) {
419
                $level--;
420
421
                if ($level === 0) {
422
                    $stream->position = $i - 1;
423
                    break;
424
                }
425
            }
426
427
            $result .= $token->value;
428
        }
429
430
        return $result;
431
    }
432
}
433