Completed
Push — master ( 7bef7a...470c23 )
by Richard
07:03
created

RuleParser   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 94.91%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 12
dl 0
loc 475
ccs 205
cts 216
cp 0.9491
rs 6.8
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A add_symbols_to() 0 21 1
A add_symbols_for_comments() 0 5 1
B add_symbols_for_variables_to() 0 34 5
A add_symbols_for_properties_to() 0 15 3
A add_symbols_for_rules_to() 0 18 4
A add_symbols_for_schemas_to() 0 8 2
A parse() 0 6 1
A root() 0 15 4
A top_level_statement() 0 19 3
A is_current_token_to_be_dropped() 0 12 4
A trim_explanation() 0 5 1
A rule_mode() 0 8 1
A string() 0 10 2
A variable() 0 19 3
A schema() 0 10 2
A variable_assignment() 0 9 2
A rule_declaration() 0 16 3
A add_variable() 0 8 2
A get_variable() 0 6 2
A add_predefined_variables() 0 5 2
A purge_predefined_variables() 0 5 2
A maybe_fetch_argument_delimiter() 0 6 2
A fetch_string() 0 4 1
A fetch_variable() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like RuleParser 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 RuleParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/******************************************************************************
3
 * An implementation of dicto (scg.unibe.ch/dicto) in and for PHP.
4
 *
5
 * Copyright (c) 2016 Richard Klees <[email protected]>
6
 * 
7
 * This software is licensed under The MIT License. You should have received
8
 * a copy of the license along with the code.
9
 */
10
11
namespace Lechimp\Dicto\Definition;
12
13
use Lechimp\Dicto\Rules\Ruleset;
14
use Lechimp\Dicto\Variables as V;
15
use Lechimp\Dicto\Rules as R;
16
17
/**
18
 * Parser for Rulesets.
19
 */
20
class RuleParser extends Parser implements ArgumentParser {
21
    const EXPLANATION_RE = "[/][*][*](([^*]|([*][^/]))*)[*][/]";
22
    const SINGLE_LINE_COMMENT_RE = "[/][/]([^\n]*)";
23
    const MULTI_LINE_COMMENT_RE = "[/][*](([^*]|([*][^/]))*)[*][/]";
24
    const ASSIGNMENT_RE = "(\w+)\s*=\s*";
25
    const STRING_RE = "[\"]((([\\\\][\"])|[^\"])+)[\"]";
26
    const RULE_MODE_RE = "must|can(not)?";
27
28
    /**
29
     * @var V\Variable[]
30
     */
31
    protected $predefined_variables;
32
33
    /**
34
     * @var R\Schema[]
35
     */
36
    protected $schemas;
37
38
    /**
39
     * @var R\Property[]
40
     */
41
    protected $properties;
42
43
    /**
44
     * @var V\Variable[]
45
     */
46
    protected $variables = array();
47
48
    /**
49
     * @var R\Rule[]
50
     */
51
    protected $rules = array();
52
53
    /**
54
     * @var string|null
55
     */
56
    protected $last_explanation = null;
57
58
    /**
59
     * @param   V\Variable[]    $predefined_variables
60
     * @param   R\Schema[]      $schemas
61
     * @param   V\Property[]    $properties
62
     */
63 40
    public function __construct( array $predefined_variables
64
                               , array $schemas
65
                               , array $properties) {
66
        $this->predefined_variables = array_map(function(V\Variable $v) {
67 40
            return $v;
68 40
        }, $predefined_variables);
69
        $this->schemas = array_map(function(R\Schema $s) {
70 40
            return $s;
71 40
        }, $schemas);
72
        $this->properties = array_map(function(V\Property $p) {
73 40
            return $p;
74 40
        }, $properties);
75 40
        parent::__construct();
76 40
    }
77
78
    // Definition of symbols in the parser
79
80
    /**
81
     * @inheritdocs
82
     */
83 40
    protected function add_symbols_to(SymbolTable $table) {
84 40
        $this->add_symbols_for_comments($table);
85
86 40
        $this->add_symbols_for_variables_to($table);
87
88 40
        $this->add_symbols_for_rules_to($table);
89
90
        // Strings
91 40
        $table->symbol(self::STRING_RE);
92
93
        // Assignment 
94 40
        $table->symbol(self::ASSIGNMENT_RE);
95
96
        // Names
97
        $table->literal("\w+", function (array &$matches) {
98 35
                return $this->get_variable($matches[0]);
99 40
            });
100
101
        // End of statement
102 40
        $table->symbol("\n");
103 40
    }
104
105
    /**
106
     * @param   SymbolTable
107
     * @return  null
108
     */
109 40
    protected function add_symbols_for_comments(SymbolTable $table) {
110 40
        $table->symbol(self::EXPLANATION_RE);
111 40
        $table->symbol(self::SINGLE_LINE_COMMENT_RE);
112 40
        $table->symbol(self::MULTI_LINE_COMMENT_RE);
113 40
    }
114
115
    /**
116
     * @param   SymbolTable
117
     * @return  null
118
     */
119 40
    protected function add_symbols_for_variables_to(SymbolTable $table) {
120
        // Any
121 40
        $table->operator("{")
122
            ->null_denotation_is(function() {
123 8
                $arr = array();
124 8
                while(true) {
125 8
                    $arr[] = $this->variable(0);
126 8
                    if ($this->is_current_token_operator("}")) {
127 8
                        $this->advance_operator("}");
128
                        // short circuit
129 8
                        if (count($arr) == 1) {
130 2
                            return $arr[0];
131
                        }
132 6
                        return new V\Any($arr);
133
                    }
134 6
                    $this->advance_operator(",");
135 6
                }
136 40
            });
137 40
        $table->operator("}");
138 40
        $table->operator(",");
139
140
        // Except
141 40
        $table->symbol("except", 10)
142
            ->left_denotation_is(function($left, array &$matches) {
0 ignored issues
show
Unused Code introduced by
The parameter $matches is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143 4
                if (!($left instanceof V\Variable)) {
144
                    throw new ParserException
145
                        ("Expected a variable at the left of except.");
146
                }
147 4
                $right = $this->variable(10);
148 4
                return new V\Except($left, $right);
149 40
            });
150
151 40
        $this->add_symbols_for_properties_to($table, $this->properties);
152 40
    }
153
154
    /**
155
     * @param   SymbolTable     $table
156
     * @param   V\Property[]    $properties
157
     * @return  null
158
     */
159 40
    protected function add_symbols_for_properties_to(SymbolTable $table, array &$properties) {
160 40
        foreach ($properties as $property) {
161 40
            $table->symbol($property->parse_as().":", 20)
162
                ->left_denotation_is(function($left) use ($property) {
163 8
                    if (!($left instanceof V\Variable)) {
164
                        throw new ParserException
165
                            ("Expected a variable at the left of \"with name:\".");
166
                    }
167 8
                    $this->is_start_of_rule_arguments = true;
168 8
                    $arguments = $property->fetch_arguments($this);
169 8
                    assert('is_array($arguments)');
170 8
                    return new V\WithProperty($left, $property, $arguments);
171 40
                });
172 40
        }
173 40
    }
174
175
    /**
176
     * @param   SymbolTable
177
     * @return  null
178
     */
179 40
    protected function add_symbols_for_rules_to(SymbolTable $table) {
180
        // Rules
181 40
        $table->symbol("only");
182 40
        $table->symbol(self::RULE_MODE_RE, 0)
183
            ->null_denotation_is(function (array &$matches) {
184 11
                if ($matches[0] == "can") {
185 3
                    return R\Rule::MODE_ONLY_CAN;
186
                }
187 8
                if ($matches[0] == "must") {
188 4
                    return R\Rule::MODE_MUST;
189
                }
190 4
                if ($matches[0] == "cannot") {
191 4
                    return R\Rule::MODE_CANNOT;
192
                }
193
                throw new \LogicException("Unexpected \"".$matches[0]."\".");
194 40
            });
195 40
        $this->add_symbols_for_schemas_to($table, $this->schemas);
196 40
    }
197
198
    /**
199
     * @param   SymbolTable     $table
200
     * @param   R\Schema[]    $schemas
201
     * @return  null
202
     */
203 40
    protected function add_symbols_for_schemas_to(SymbolTable $table, array &$schemas) {
204 40
        foreach ($schemas as $schema) {
205 40
            $table->symbol($schema->name())
206 40
                ->null_denotation_is(function(array &$_) use ($schema) {
0 ignored issues
show
Unused Code introduced by
The parameter $_ is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
207 11
                    return $schema;
208 40
                });
209 40
        }
210 40
    }
211
212
    // IMPLEMENTATION OF Parser
213
214
    /**
215
     * @return  Ruleset
216
     */
217 40
    public function parse($source) {
218 40
        $this->variables = array();
219 40
        $this->rules = array();
220 40
        $this->add_predefined_variables();
221 40
        return parent::parse($source);
222
    }
223
224
    /**
225
     * Root expression for the parser is some whitespace or comment where a
226
     * top level statement is in the middle.
227
     *
228
     * @return  Ruleset 
229
     */
230 27
    protected function root() {
231 27
        while (true) {
232
            // drop empty lines
233 27
            while ($tok = $this->is_current_token_to_be_dropped()) {
234 11
                $this->advance($tok);
235 11
            }
236 27
            if ($this->is_end_of_file_reached()) {
237 27
                break;
238
            }
239
240 26
            $this->top_level_statement();
241 26
        }
242 27
        $this->purge_predefined_variables();
243 27
        return new Ruleset($this->variables, $this->rules);
244
    }
245
246
    /**
247
     * Parses the top level statements in the rules file.
248
     *
249
     * @return  null
250
     */
251 26
    public function top_level_statement() {
252
        // A top level statements is either..
253
        // ..an explanation
254 26
        if ($this->is_current_token_matched_by(self::EXPLANATION_RE)) {
255 5
            $m = $this->current_match();
256 5
            $this->last_explanation = $this->trim_explanation($m[1]);
257 5
            $this->advance(self::EXPLANATION_RE);
258 5
        }
259
        // ..an assignment to a variable.
260 26
        elseif ($this->is_current_token_matched_by(self::ASSIGNMENT_RE)) {
261 16
            $this->variable_assignment();
262 16
            $this->last_explanation = null;
263 16
        }
264
        // ..or a rule declaration
265
        else {
266 11
            $this->rule_declaration();
267 11
            $this->last_explanation = null;
268
        }
269 26
    }
270
271
    /**
272
     * Returns currently matched whitespace or comment token if there is any.
273
     *
274
     * @return string|null
275
     */
276 27
    public function is_current_token_to_be_dropped() {
277 27
        if ($this->is_current_token_matched_by("\n")) {
278 11
            return "\n";
279
        }
280 27
        if ($this->is_current_token_matched_by(self::SINGLE_LINE_COMMENT_RE)) {
281 2
            return self::SINGLE_LINE_COMMENT_RE;
282
        }
283 27
        if ($this->is_current_token_matched_by(self::MULTI_LINE_COMMENT_RE)) {
284 2
            return self::MULTI_LINE_COMMENT_RE;
285
        }
286 27
        return null;
287
    }
288
289
    /**
290
     * @param   string
291
     * @return  string
292
     */
293 5
    protected function trim_explanation($content) {
294 5
        return trim(
295 5
            preg_replace("%\s*\n\s*([*]\s*)?%s", "\n", $content)
296 5
        );
297
    }
298
299
    // EXPRESSION TYPES
300
301
    /**
302
     * Fetch a rule mode from the stream.
303
     *
304
     * @return mixed
305
     */
306 11
    protected function rule_mode() {
307 11
        $this->is_current_token_matched_by(self::RULE_MODE_RE);
308 11
        $t = $this->current_symbol();
309 11
        $m = $this->current_match();
310 11
        $this->fetch_next_token();
311 11
        $mode = $t->null_denotation($m);
312 11
        return $mode;
313
    }
314
315
    /**
316
     * Fetch a string from the stream.
317
     *
318
     * @return  string
319
     */
320 16
    protected function string() {
321 16
        if (!$this->is_current_token_matched_by(self::STRING_RE)) {
322
            throw new ParserException("Expected string.");
323
        }
324 16
        $m = $this->current_match();
325 16
        $this->fetch_next_token();
326 16
        return  str_replace("\\\"", "\"",
327 16
                    str_replace("\\n", "\n",
328 16
                        $m[1]));
329
    }
330
331
    /**
332
     * Fetch a variable from the stream.
333
     *
334
     * @return  V\Variable
335
     */
336 35
    protected function variable($right_binding_power = 0) {
337 35
        $t = $this->current_symbol();
338 35
        $m = $this->current_match();
339 35
        $this->fetch_next_token();
340 35
        $left = $t->null_denotation($m);
341
342 35
        while ($right_binding_power < $this->token[0]->binding_power()) {
343 11
            $t = $this->current_symbol();
344 11
            $m = $this->current_match();
345 11
            $this->fetch_next_token();
346 11
            $left = $t->left_denotation($left, $m);
347 11
        }
348
349 35
        if (!($left instanceof V\Variable)) {
350
            throw new ParserException("Expected variable.");
351
        }
352
353 35
        return $left;
354
    }
355
356
    /**
357
     * Fetch a rule schema and its arguments from the stream.
358
     *
359
     * @return  array   (R\Schema, array)
360
     */
361 11
    protected function schema() {
362 11
        $t = $this->current_symbol();
363 11
        $m = $this->current_match();
364 11
        $this->fetch_next_token();
365 11
        $schema = $t->null_denotation($m);
366 11
        if (!($schema instanceof R\Schema)) {
367
            throw new ParserException("Expected name of a rule schema.");
368
        }
369 11
        return $schema;
370
    }
371
372
    // TOP LEVEL STATEMENTS
373
374
    /**
375
     * Process a variable assignment.
376
     *
377
     * @return  null
378
     */
379 16
    protected function variable_assignment() {
380 16
        $m = $this->current_match(); 
381 16
        $this->fetch_next_token();
382 16
        $def = $this->variable();
383 16
        if ($this->last_explanation !== null) {
384 4
            $def = $def->withExplanation($this->last_explanation);
385 4
        }
386 16
        $this->add_variable($m[1], $def);
387 16
    }
388
389
    /**
390
     * Process a rule declaration.
391
     *
392
     * @return  null
393
     */
394 11
    protected function rule_declaration() {
395 11
        if ($this->is_current_token_matched_by("only")) {
396 3
            $this->advance("only");
397 3
        }
398 11
        $var = $this->variable();
399 11
        $mode = $this->rule_mode();
400 11
        $schema = $this->schema();
401 11
        $this->is_start_of_rule_arguments = true;
402 11
        $arguments = $schema->fetch_arguments($this);
403 11
        assert('is_array($arguments)');
404 11
        $rule = new R\Rule($mode, $var, $schema, $arguments);
405 11
        if ($this->last_explanation !== null) {
406 1
            $rule= $rule->withExplanation($this->last_explanation);
407 1
        }
408 11
        $this->rules[] = $rule;
409 11
    }
410
411
412
    // HANDLING OF VARIABLES
413
414
    /**
415
     * Add a variable to the variables currently known.
416
     *
417
     * @param   string      $name
418
     * @param   V\Variable  $def
419
     * @return null
420
     */
421 40
    protected function add_variable($name, V\Variable $def) {
422 40
        assert('is_string($name)');
423 40
        if (array_key_exists($name, $this->variables)) {
424
            throw new ParserException("Variable '$name' already defined.");
425
        }
426 40
        assert('$def instanceof Lechimp\\Dicto\\Variables\\Variable');
427 40
        $this->variables[$name] = $def->withName($name);
428 40
    }
429
430
    /**
431
     * Get a predefined variable.
432
     *
433
     * @param   string  $name
434
     * @return  V\Variable
435
     */
436 35
    protected function get_variable($name) {
437 35
        if (!array_key_exists($name, $this->variables)) {
438
            throw new ParserException("Unknown variable '$name'.");
439
        }
440 35
        return $this->variables[$name];
441
    }
442
443
    /**
444
     * Add all predefined variables to the current set of variables.
445
     *
446
     * @return null
447
     */
448 40
    protected function add_predefined_variables() {
449 40
        foreach ($this->predefined_variables as $predefined_var) {
450 40
            $this->add_variable($predefined_var->name(), $predefined_var);
451 40
        }
452 40
    }
453
454
    /**
455
     * Purge all predefined variables from the current set of variables.
456
     *
457
     * @return null
458
     */
459 27
    protected function purge_predefined_variables() {
460 27
        foreach ($this->predefined_variables as $predefined_var) {
461 27
            unset($this->variables[$predefined_var->name()]);
462 27
        }
463 27
    }
464
465
    // IMPLEMENTATION OF ArgumentParser
466
467
    /**
468
     * @var bool
469
     */
470
    protected $is_start_of_rule_arguments = false;
471
472 16
    protected function maybe_fetch_argument_delimiter() {
473 16
        if (!$this->is_start_of_rule_arguments) {
474
            $this->advance_operator(",");
475
            $this->is_start_of_rule_arguments = false;
476
        }
477 16
    }
478
479
    /**
480
     * @inheritdoc
481
     */
482 12
    public function fetch_string() {
483 12
        $this->maybe_fetch_argument_delimiter();
484 12
        return $this->string();
485
    }
486
487
    /**
488
     * @inheritdoc
489
     */
490 8
    public function fetch_variable() {
491 8
        $this->maybe_fetch_argument_delimiter();
492 8
        return $this->variable();
493
    }
494
}
495