Passed
Push — develop ( ef7d65...ebf1a0 )
by Maarten de
01:47
created

AbstractParser   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 148
c 1
b 0
f 0
dl 0
loc 380
rs 8.72
wmc 46

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 27 2
B parseComparison() 0 32 6
A parseValuePath() 0 21 4
A parseAttributePath() 0 28 5
A parseParentheses() 0 11 2
B parseInner() 0 42 10
A updateValuePathAttributePath() 0 25 5
B parseConnective() 0 32 8
A parseNegation() 0 28 4

How to fix   Complexity   

Complex Class

Complex classes like AbstractParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractParser, and based on these observations, apply Extract Interface, too.

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.
72
     *
73
     * @param string $input
74
     *
75
     * @return AST\Node|AST\Path|null
76
     */
77
    public abstract function parse(string $input);
0 ignored issues
show
Coding Style introduced by
The abstract declaration must precede the visibility declaration
Loading history...
78
79
    /**
80
     * Parse inner.
81
     *
82
     * @param Tokenizer\Stream $stream
83
     * @param bool             $inValuePath
84
     *
85
     * @throws TokenizerException|\Nette\Tokenizer\Exception
86
     *
87
     * @return AST\Node|AST\Path|null
88
     */
89
    protected function parseInner(Tokenizer\Stream $stream, bool $inValuePath = false)
90
    {
91
        $node = null;
92
93
        if ($stream->isNext(self::T_PAREN_OPEN)) {
94
            // Parentheses
95
            $node = $this->parseParentheses($stream);
96
97
            if ($node === null) {
98
                return null;
99
            }
100
        } elseif ($stream->isNext(self::T_NEGATION)) {
101
            // Negation
102
            $node = $this->parseNegation($stream, $inValuePath);
103
104
            if ($node === null) {
105
                return null;
106
            }
107
        } elseif ($stream->isNext(self::T_NAME)) {
108
            // Comparison or value path
109
            $node = $this->parseAttributePath($stream, $inValuePath);
110
        }
111
112
        if ($node !== null && $node instanceof AST\Node) {
113
            // Logical connective
114
            if ($stream->isNext(self::T_LOG_OP)) {
115
                return $this->parseConnective($stream, $node, $inValuePath);
116
            }
117
118
            return $node;
119
        }
120
121
        if ($inValuePath === true) {
122
            throw new InvalidValuePathException();
123
        }
124
125
        throw new TokenizerException(
126
            sprintf(
127
                'Expected an attribute/value path, opening parenthesis or a negation, got "%s".',
128
                $stream->nextValue() ?? ''
129
            ),
130
            $stream
131
        );
132
    }
133
134
    /**
135
     * Parse filter in parentheses.
136
     *
137
     * @param Tokenizer\Stream $stream
138
     *
139
     * @throws \Nette\Tokenizer\Exception
140
     *
141
     * @return AST\Node|AST\Path|null
142
     */
143
    protected function parseParentheses(Tokenizer\Stream $stream)
144
    {
145
        $stream->matchNext(self::T_PAREN_OPEN);
146
        $filter = $stream->joinUntil(self::T_PAREN_CLOSE);
147
        $stream->matchNext(self::T_PAREN_CLOSE);
148
149
        if (empty($filter)) {
150
            return null;
151
        }
152
153
        return $this->parse($filter);
154
    }
155
156
    /**
157
     * Parse negation.
158
     *
159
     * @param Tokenizer\Stream $stream
160
     * @param bool             $inValuePath
161
     *
162
     * @throws TokenizerException|\Nette\Tokenizer\Exception
163
     *
164
     * @return AST\Node|null
165
     */
166
    protected function parseNegation(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
167
    {
168
        $stream->matchNext(self::T_NEGATION);
169
        $stream->matchNext(self::T_PAREN_OPEN);
170
        $filter = $stream->joinUntil(self::T_PAREN_CLOSE);
171
        $stream->matchNext(self::T_PAREN_CLOSE);
172
173
        if (empty($filter)) {
174
            return null;
175
        }
176
177
        $node = $this->parseInner($this->tokenizer->tokenize($filter), $inValuePath);
178
179
        if ($node === null) {
180
            return null;
181
        }
182
183
        if ($node instanceof AST\Node === false) {
184
            throw new TokenizerException(
185
                sprintf(
186
                    'Invalid filter in negation, got "%s".',
187
                    $stream->nextValue() ?? ''
188
                ),
189
                $stream
190
            );
191
        }
192
193
        return new AST\Negation($node);
194
    }
195
196
    /**
197
     * Parse attribute path.
198
     *
199
     * @param Tokenizer\Stream $stream
200
     * @param bool             $inValuePath
201
     *
202
     * @throws TokenizerException|\Nette\Tokenizer\Exception
203
     *
204
     * @return AST\Node
205
     */
206
    protected function parseAttributePath(Tokenizer\Stream $stream, bool $inValuePath = false): AST\Node
207
    {
208
        $name = $stream->matchNext(self::T_NAME)->value;
209
210
        // Attribute scheme
211
        $scheme = null;
212
213
        if (strpos($name, ':') !== false) {
214
            /** @var int $lastColonPos */
215
            $lastColonPos = strrpos($name, ':');
216
            $scheme = substr($name, 0, $lastColonPos);
217
            $name = substr($name, $lastColonPos + 1);
218
        }
219
220
        // Attribute path
221
        $attributePath = new AST\AttributePath($scheme, explode('.', $name));
222
223
        // Value path (only if not in value path already
224
        if ($stream->isNext(self::T_BRACKET_OPEN)) {
225
            if ($inValuePath === true || count($attributePath) !== 1) {
226
                throw new InvalidValuePathException($stream);
227
            }
228
229
            return $this->parseValuePath($stream, $attributePath);
230
        }
231
232
        // Comparison
233
        return $this->parseComparison($stream, $attributePath);
234
    }
235
236
    /**
237
     * Parse value path.
238
     *
239
     * @param Tokenizer\Stream  $stream
240
     * @param AST\AttributePath $attributePath
241
     *
242
     * @throws TokenizerException|\Nette\Tokenizer\Exception
243
     *
244
     * @return AST\ValuePath
245
     */
246
    protected function parseValuePath(Tokenizer\Stream $stream, AST\AttributePath $attributePath): AST\ValuePath
247
    {
248
        // Parse node between brackets.
249
        $stream->matchNext(self::T_BRACKET_OPEN);
250
251
        $node = $this->parseInner($stream, true);
252
253
        if (
254
            $node instanceof AST\Connective === false
255
            && $node instanceof AST\Comparison === false
256
            && $node instanceof AST\Negation === false
257
        ) {
258
            throw new InvalidValuePathException($stream);
259
        }
260
261
        $stream->matchNext(self::T_BRACKET_CLOSE);
262
263
        // Correct attribute path for node.
264
        $this->updateValuePathAttributePath($attributePath, $node);
265
266
        return new AST\ValuePath($attributePath, $node);
267
    }
268
269
    /**
270
     * Parse comparison.
271
     *
272
     * @param Tokenizer\Stream  $stream
273
     * @param AST\AttributePath $attributePath
274
     *
275
     * @throws \Nette\Tokenizer\Exception
276
     *
277
     * @return AST\Connective|AST\Comparison
278
     */
279
    protected function parseComparison(
280
        Tokenizer\Stream $stream,
281
        AST\AttributePath $attributePath
282
    ) {
283
        $operator = trim($stream->matchNext(self::T_COMP_OP)->value);
284
        $value = null;
285
286
        if (strcasecmp($operator, 'pr') <> 0) {
287
            $value = $stream->matchNext(self::T_STRING, self::T_NUMBER, self::T_BOOL, self::T_NULL);
288
289
            switch ($value->type) {
290
                case self::T_STRING:
291
                    $value = trim($value->value, '"');
292
                    break;
293
                case self::T_NUMBER:
294
                    if (strpos($value->value, '.') !== false) {
295
                        $value = floatval($value->value);
296
                        break;
297
                    }
298
299
                    $value = intval($value->value);
300
                    break;
301
                case self::T_BOOL:
302
                    $value = strcasecmp($value->value, 'true') === 0;
303
                    break;
304
                default:
305
                    $value = null;
306
                    break;
307
            }
308
        }
309
310
        return new AST\Comparison($attributePath, $operator, $value);
311
    }
312
313
    /**
314
     * Parse logical connective (and/or).
315
     *
316
     * @param Tokenizer\Stream $stream
317
     * @param AST\Node         $leftNode
318
     * @param bool             $inValuePath
319
     *
320
     * @throws TokenizerException|\Nette\Tokenizer\Exception
321
     *
322
     * @return AST\Connective
323
     */
324
    protected function parseConnective(
325
        Tokenizer\Stream $stream,
326
        AST\Node $leftNode,
327
        bool $inValuePath = false
328
    ): AST\Connective {
329
        $logOp = trim($stream->matchNext(self::T_LOG_OP)->value);
330
331
        $isConjunction = strcasecmp($logOp, 'and') === 0;
332
333
        // Parse right hand node
334
        $rightNode = $this->parseInner($stream, $inValuePath);
335
336
        if ($rightNode === null || $rightNode instanceof AST\Node === false) {
337
            throw new TokenizerException('Invalid right hand side of comparison.', $stream);
338
        }
339
340
        // Connective nodes
341
        $nodes = [$leftNode, $rightNode];
342
343
        // Merge consecutive connectives of the same type into one connective.
344
        if (
345
            ($rightNode instanceof AST\Conjunction && $isConjunction === true)
346
            || ($rightNode instanceof AST\Disjunction && $isConjunction === false)
347
        ) {
348
            $rightNodes = $rightNode->getNodes();
349
350
            $nodes = array_merge([$leftNode], $rightNodes);
351
        }
352
353
        return $isConjunction === true
354
            ? new AST\Conjunction($nodes)
355
            : new AST\Disjunction($nodes);
356
    }
357
358
    /**
359
     * Prepend attribute path to node.
360
     *
361
     * @param AST\AttributePath $attributePath
362
     * @param AST\Node          $node
363
     *
364
     * @throws TokenizerException
365
     *
366
     * @return AST\Node
367
     */
368
    protected function updateValuePathAttributePath(AST\AttributePath $attributePath, AST\Node $node): AST\Node
369
    {
370
        if ($node instanceof AST\Comparison) {
371
            $names = array_merge($attributePath->getNames(), $node->getAttributePath()->getNames());
372
373
            $node->setAttributePath(new AST\AttributePath($attributePath->getSchema(), $names));
374
375
            return $node;
376
        }
377
378
        if ($node instanceof AST\Negation) {
379
            $node->setNode($this->updateValuePathAttributePath($attributePath, $node->getNode()));
380
381
            return $node;
382
        }
383
384
        if ($node instanceof AST\Connective) {
385
            foreach ($node->getNodes() as $subNode) {
386
                $this->updateValuePathAttributePath($attributePath, $subNode);
387
            }
388
389
            return $node;
390
        }
391
392
        throw new InvalidValuePathException();
393
    }
394
}
395