Passed
Push — develop ( cb60d7...ef7d65 )
by Maarten de
07:14
created

AbstractParser::parseAttributePath()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 27
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
nc 6
nop 2
dl 0
loc 27
rs 9.5555
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\InvalidValuePathException;
8
use Cloudstek\SCIM\FilterParser\Exception\TokenizerException;
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;
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
        $this->tokenizer = new Tokenizer\Tokenizer(
63
            $patterns,
64
            'i'
65
        );
66
67
        $this->mode = $mode;
68
    }
69
70
    /**
71
     * Parse inner.
72
     *
73
     * @param Tokenizer\Stream $stream
74
     * @param bool             $inValuePath
75
     *
76
     * @throws TokenizerException|\Nette\Tokenizer\Exception
77
     *
78
     * @return AST\Node|null
79
     */
80
    protected function parseInner(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
81
    {
82
        $node = null;
83
84
        if ($stream->isNext(self::T_PAREN_OPEN)) {
85
            // Parentheses
86
            $node = $this->parseParentheses($stream);
87
88
            if ($node === null) {
89
                return null;
90
            }
91
        } elseif ($stream->isNext(self::T_NEGATION)) {
92
            // Negation
93
            $node = $this->parseNegation($stream, $inValuePath);
94
95
            if ($node === null) {
96
                return null;
97
            }
98
        } elseif ($stream->isNext(self::T_NAME)) {
99
            // Comparison or value path
100
            $node = $this->parseAttributePath($stream, $inValuePath);
101
        }
102
103
        if ($node !== null) {
104
            // Logical connective
105
            if ($stream->isNext(self::T_LOG_OP)) {
106
                return $this->parseConnective($stream, $node, $inValuePath);
107
            }
108
109
            return $node;
110
        }
111
112
        if ($inValuePath === true) {
113
            throw new InvalidValuePathException();
114
        }
115
116
        throw new TokenizerException(
117
            sprintf(
118
                'Expected an attribute/value path, opening parenthesis or a negation, got "%s".',
119
                $stream->nextValue()
120
            ),
121
            $stream
122
        );
123
    }
124
125
    /**
126
     * Parse filter in parentheses.
127
     *
128
     * @param Tokenizer\Stream $stream
129
     *
130
     * @throws \Nette\Tokenizer\Exception
131
     *
132
     * @return AST\Node|null
133
     */
134
    protected function parseParentheses(Tokenizer\Stream $stream): ?AST\Node
135
    {
136
        $stream->matchNext(self::T_PAREN_OPEN);
137
        $filter = $stream->joinUntil(self::T_PAREN_CLOSE);
138
        $stream->matchNext(self::T_PAREN_CLOSE);
139
140
        if (empty($filter)) {
141
            return null;
142
        }
143
144
        return $this->parse($filter);
0 ignored issues
show
Bug introduced by
The method parse() does not exist on Cloudstek\SCIM\FilterParser\AbstractParser. Did you maybe mean parseInner()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

144
        return $this->/** @scrutinizer ignore-call */ parse($filter);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
145
    }
146
147
    /**
148
     * Parse negation.
149
     *
150
     * @param Tokenizer\Stream $stream
151
     * @param bool             $inValuePath
152
     *
153
     * @throws TokenizerException|\Nette\Tokenizer\Exception
154
     *
155
     * @return AST\Node|null
156
     */
157
    protected function parseNegation(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
158
    {
159
        $stream->matchNext(self::T_NEGATION);
160
        $stream->matchNext(self::T_PAREN_OPEN);
161
        $filter = $stream->joinUntil(self::T_PAREN_CLOSE);
162
        $stream->matchNext(self::T_PAREN_CLOSE);
163
164
        if (empty($filter)) {
165
            return null;
166
        }
167
168
        $node = $this->parseInner($this->tokenizer->tokenize($filter), $inValuePath);
169
170
        return new AST\Negation($node);
171
    }
172
173
    /**
174
     * Parse attribute path.
175
     *
176
     * @param Tokenizer\Stream $stream
177
     * @param bool             $inValuePath
178
     *
179
     * @throws TokenizerException|\Nette\Tokenizer\Exception
180
     *
181
     * @return AST\Node
182
     */
183
    protected function parseAttributePath(Tokenizer\Stream $stream, bool $inValuePath = false): AST\Node
184
    {
185
        $name = $stream->matchNext(self::T_NAME)->value;
186
187
        // Attribute scheme
188
        $scheme = null;
189
190
        if (strpos($name, ':') !== false) {
191
            $lastColonPos = strrpos($name, ':');
192
            $scheme = substr($name, 0, $lastColonPos);
193
            $name = substr($name, $lastColonPos + 1);
194
        }
195
196
        // Attribute path
197
        $attributePath = new AST\AttributePath($scheme, explode('.', $name));
198
199
        // Value path (only if not in value path already
200
        if ($stream->isNext(self::T_BRACKET_OPEN)) {
201
            if ($inValuePath === true || count($attributePath) !== 1) {
202
                throw new InvalidValuePathException($stream);
203
            }
204
205
            return $this->parseValuePath($stream, $attributePath);
206
        }
207
208
        // Comparison
209
        return $this->parseComparison($stream, $attributePath);
210
    }
211
212
    /**
213
     * Parse value path.
214
     *
215
     * @param Tokenizer\Stream  $stream
216
     * @param AST\AttributePath $attributePath
217
     *
218
     * @throws TokenizerException|\Nette\Tokenizer\Exception
219
     *
220
     * @return AST\ValuePath
221
     */
222
    protected function parseValuePath(Tokenizer\Stream $stream, AST\AttributePath $attributePath): AST\ValuePath
223
    {
224
        // Parse node between brackets.
225
        $stream->matchNext(self::T_BRACKET_OPEN);
226
227
        $node = $this->parseInner($stream, true);
228
229
        if (
230
            $node instanceof AST\Connective === false
231
            && $node instanceof AST\Comparison === false
232
            && $node instanceof AST\Negation === false
233
        ) {
234
            throw new InvalidValuePathException($stream);
235
        }
236
237
        $stream->matchNext(self::T_BRACKET_CLOSE);
238
239
        // Correct attribute path for node.
240
        $this->updateValuePathAttributePath($attributePath, $node);
241
242
        return new AST\ValuePath($attributePath, $node);
243
    }
244
245
    /**
246
     * Parse comparison.
247
     *
248
     * @param Tokenizer\Stream  $stream
249
     * @param AST\AttributePath $attributePath
250
     *
251
     * @throws \Nette\Tokenizer\Exception
252
     *
253
     * @return AST\Connective|AST\Comparison
254
     */
255
    protected function parseComparison(
256
        Tokenizer\Stream $stream,
257
        AST\AttributePath $attributePath
258
    ) {
259
        $operator = trim($stream->matchNext(self::T_COMP_OP)->value);
260
        $value = null;
261
262
        if (strcasecmp($operator, 'pr') <> 0) {
263
            $value = $stream->matchNext(self::T_STRING, self::T_NUMBER, self::T_BOOL, self::T_NULL);
264
265
            switch ($value->type) {
266
                case self::T_STRING:
267
                    $value = trim($value->value, '"');
268
                    break;
269
                case self::T_NUMBER:
270
                    if (strpos($value->value, '.') !== false) {
271
                        $value = floatval($value->value);
272
                        break;
273
                    }
274
275
                    $value = intval($value->value);
276
                    break;
277
                case self::T_BOOL:
278
                    $value = strcasecmp($value->value, 'true') === 0;
279
                    break;
280
                default:
281
                    $value = null;
282
                    break;
283
            }
284
        }
285
286
        return new AST\Comparison($attributePath, $operator, $value);
287
    }
288
289
    /**
290
     * Parse logical connective (and/or).
291
     *
292
     * @param Tokenizer\Stream $stream
293
     * @param AST\Node         $leftNode
294
     * @param bool             $inValuePath
295
     *
296
     * @throws TokenizerException|\Nette\Tokenizer\Exception
297
     *
298
     * @return AST\Connective
299
     */
300
    protected function parseConnective(
301
        Tokenizer\Stream $stream,
302
        AST\Node $leftNode,
303
        bool $inValuePath = false
304
    ): AST\Connective {
305
        $logOp = trim($stream->matchNext(self::T_LOG_OP)->value);
306
307
        $isConjunction = strcasecmp($logOp, 'and') === 0;
308
309
        // Parse right hand node
310
        $rightNode = $this->parseInner($stream, $inValuePath);
311
312
        // Connective nodes
313
        $nodes = [$leftNode, $rightNode];
314
315
        // Merge consecutive connectives of the same type into one connective.
316
        if (
317
            ($rightNode instanceof AST\Conjunction && $isConjunction === true)
318
            || ($rightNode instanceof AST\Disjunction && $isConjunction === false)
319
        ) {
320
            $rightNodes = $rightNode->getNodes();
321
322
            $nodes = array_merge([$leftNode], $rightNodes);
323
        }
324
325
        return $isConjunction === true
326
            ? new AST\Conjunction($nodes)
327
            : new AST\Disjunction($nodes);
328
    }
329
330
    /**
331
     * Prepend attribute path to node.
332
     *
333
     * @param AST\AttributePath $attributePath
334
     * @param AST\Node          $node
335
     *
336
     * @throws TokenizerException
337
     *
338
     * @return AST\Node
339
     */
340
    protected function updateValuePathAttributePath(AST\AttributePath $attributePath, AST\Node $node): AST\Node
341
    {
342
        if ($node instanceof AST\Comparison) {
343
            $names = array_merge($attributePath->getNames(), $node->getAttributePath()->getNames());
344
345
            $node->setAttributePath(new AST\AttributePath($attributePath->getSchema(), $names));
346
347
            return $node;
348
        }
349
350
        if ($node instanceof AST\Negation) {
351
            $node->setNode($this->updateValuePathAttributePath($attributePath, $node->getNode()));
352
353
            return $node;
354
        }
355
356
        if ($node instanceof AST\Connective) {
357
            foreach ($node->getNodes() as $subNode) {
358
                $this->updateValuePathAttributePath($attributePath, $subNode);
359
            }
360
361
            return $node;
362
        }
363
364
        throw new InvalidValuePathFilterException();
0 ignored issues
show
Bug introduced by
The type Cloudstek\SCIM\FilterPar...aluePathFilterException 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...
365
    }
366
}
367