Completed
Push — master ( cc2765...340ff8 )
by Richard
08:19
created

RuleParser   C

Complexity

Total Complexity 41

Size/Duplication

Total Lines 358
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Test Coverage

Coverage 93.71%

Importance

Changes 15
Bugs 0 Features 7
Metric Value
wmc 41
lcom 1
cbo 18
dl 0
loc 358
ccs 164
cts 175
cp 0.9371
rs 6.1968
c 15
b 0
f 7

18 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 85 8
A parse() 0 6 1
B root() 0 28 6
A rule_mode() 0 8 1
A string() 0 10 2
A variable() 0 19 3
A rule_schema() 0 10 2
A variable_assignment() 0 6 1
A rule_declaration() 0 12 2
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 add_schemas() 0 5 2
A add_schema() 0 6 1
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\ArgumentParser;
14
use Lechimp\Dicto\Rules\Ruleset;
15
use Lechimp\Dicto\Variables as V;
16
use Lechimp\Dicto\Rules as R;
17
18
/**
19
 * Parser for Rulesets.
20
 */
21
class RuleParser extends Parser implements ArgumentParser {
22
    const ASSIGNMENT_RE = "(\w+)\s*=\s*";
23
    const STRING_RE = "[\"]((([\\\\][\"])|[^\"])+)[\"]";
24
    const RULE_MODE_RE = "must|can(not)?";
25
26
    /**
27
     * @var Variable[]
28
     */
29
    protected $predefined_variables;
30
31 25
    public function __construct() {
32 25
        parent::__construct();
33 25
        $this->predefined_variables = array
0 ignored issues
show
Documentation Bug introduced by
It seems like array(new \Lechimp\Dicto...to\Variables\Methods()) of type array<integer,object<Lec...\Variables\\Methods>"}> is incompatible with the declared type array<integer,object<Lec...o\Definition\Variable>> of property $predefined_variables.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
34 25
            ( new V\Classes()
35 25
            , new V\Functions()
36 25
            , new V\Globals()
37 25
            , new V\Files()
38 25
            , new V\Methods()
39
            // TODO: Add some language constructs here...
40 25
            );
41
42
        $known_schemas = array
43 25
            ( new R\ContainText()
44 25
            , new R\DependOn()
45 25
            , new R\Invoke()
46 25
            );
47
48
        // Assignment 
49 25
        $this->symbol(self::ASSIGNMENT_RE);
50
51
        // Any
52 25
        $this->operator("{")
53
            ->null_denotation_is(function(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...
54 7
                $arr = array();
55 7
                while(true) {
56 7
                    $arr[] = $this->variable(0);
57 7
                    if ($this->is_current_token_operator("}")) {
58 7
                        $this->advance_operator("}");
59 7
                        return new V\Any($arr);
60
                    }
61 7
                    $this->advance_operator(",");
62 7
                }
63 25
            });
64 25
        $this->operator("}");
65 25
        $this->operator(",");
66
67
        // Except
68 25
        $this->symbol("except", 10)
69
            ->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...
70 7
                if (!($left instanceof V\Variable)) {
71
                    throw new ParserException
72
                        ("Expected a variable at the left of except.");
73
                }
74 7
                $right = $this->variable(10);
75 7
                return new V\Except($left, $right);
76 25
            });
77
78
        // WithName
79 25
        $this->symbol("with name:", 20)
80
            ->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...
81 7
                if (!($left instanceof V\Variable)) {
82
                    throw new ParserException
83
                        ("Expected a variable at the left of \"with name:\".");
84
                }
85 7
                $right = $this->string(20);
86 7
                return new V\WithName($right, $left);
87 25
            });
88
89
        // Strings
90 25
        $this->symbol(self::STRING_RE);
91
92
        // Rules
93 25
        $this->symbol("only");
94 25
        $this->symbol(self::RULE_MODE_RE, 0)
95
            ->null_denotation_is(function (array &$matches) {
96 11
                if ($matches[0] == "can") {
97 1
                    return R\Rule::MODE_ONLY_CAN;
98
                }
99 10
                if ($matches[0] == "must") {
100 7
                    return R\Rule::MODE_MUST;
101
                }
102 3
                if ($matches[0] == "cannot") {
103 3
                    return R\Rule::MODE_CANNOT;
104
                }
105
                throw new \LogicException("Unexpected \"".$matches[0]."\".");
106 25
            });
107 25
        $this->add_schemas($known_schemas);
108
109
        // Names
110
        $this->literal("\w+", function (array &$matches) {
111 20
                return $this->get_variable($matches[0]);
112 25
            });
113
114 25
        $this->symbol("\n");
115 25
    }
116
117
    // IMPLEMENTATION OF Parser
118
119
    /**
120
     * @return  Ruleset
121
     */
122 25
    public function parse($source) {
123 25
        $this->variables = array();
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
124 25
        $this->rules = array();
0 ignored issues
show
Bug introduced by
The property rules does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
125 25
        $this->add_predefined_variables();
126 25
        return parent::parse($source);
127
    }
128
129
    /**
130
     * Parses the top level statements in the rules file.
131
     *
132
     * @return  Ruleset 
133
     */
134 16
    protected function root() {
135 16
        while (true) {
136
            // drop empty lines
137 16
            while ($this->is_current_token_matched_by("\n")) {
138 5
                $this->advance("\n");
139 5
            }
140 16
            if ($this->is_end_of_file_reached()) {
141 6
                break;
142
            }
143
144
            // A top level statments is either..
145
            // ..an assignment to a variable.
146 15
            if ($this->is_current_token_matched_by(self::ASSIGNMENT_RE)) {
147 8
                $this->variable_assignment();
148 8
            }
149
            // ..or a rule declaration
150
            else {
151 11
                $this->rule_declaration();
152
            }
153
154 15
            if ($this->is_end_of_file_reached()) {
155 10
                break;
156
            }
157 6
            $this->advance("\n");
158 6
        }
159 16
        $this->purge_predefined_variables();
160 16
        return new Ruleset($this->variables, $this->rules);
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
161
    }
162
163
    // EXPRESSION TYPES
164
165
    /**
166
     * Fetch a rule mode from the stream.
167
     *
168
     * @return mixed
169
     */
170 11
    protected function rule_mode() {
171 11
        $this->is_current_token_matched_by(self::RULE_MODE_RE);
172 11
        $t = $this->current_symbol();
173 11
        $m = $this->current_match();
174 11
        $this->fetch_next_token();
175 11
        $mode = $t->null_denotation($m);
176 11
        return $mode;
177
    }
178
179
    /**
180
     * Fetch a string from the stream.
181
     *
182
     * @return  string
183
     */
184 15
    protected function string($right_binding_power = 0) {
0 ignored issues
show
Unused Code introduced by
The parameter $right_binding_power 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...
185 15
        if (!$this->is_current_token_matched_by(self::STRING_RE)) {
186
            throw new ParserException("Expected string.");
187
        }
188 15
        $m = $this->current_match();
189 15
        $this->fetch_next_token();
190 15
        return  str_replace("\\\"", "\"",
191 15
                    str_replace("\\n", "\n",
192 15
                        $m[1]));
193
    }
194
195
    /**
196
     * Fetch a variable from the stream.
197
     *
198
     * @return  V\Variable
199
     */
200 20
    protected function variable($right_binding_power = 0) {
201 20
        $t = $this->current_symbol();
202 20
        $m = $this->current_match();
203 20
        $this->fetch_next_token();
204 20
        $left = $t->null_denotation($m);
205
206 20
        while ($right_binding_power < $this->token[0]->binding_power()) {
207 10
            $t = $this->current_symbol();
208 10
            $m = $this->current_match();
209 10
            $this->fetch_next_token();
210 10
            $left = $t->left_denotation($left, $m);
211 10
        }
212
213 20
        if (!($left instanceof V\Variable)) {
214
            throw new ParserException("Expected variable.");
215
        }
216
217 20
        return $left;
218
    }
219
220
    /**
221
     * Fetch a rule schema and its arguments from the stream.
222
     *
223
     * @return  array   (R\Schema, array)
224
     */
225 11
    protected function rule_schema($right_binding_power = 0) {
0 ignored issues
show
Unused Code introduced by
The parameter $right_binding_power 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...
226 11
        $t = $this->current_symbol();
227 11
        $m = $this->current_match();
228 11
        $this->fetch_next_token();
229 11
        $schema = $t->null_denotation($m);
230 11
        if (!($schema instanceof R\Schema)) {
231
            throw new ParserException("Expected name of a rule schema.");
232
        }
233 11
        return $schema;
234
    }
235
236
    // TOP LEVEL STATEMENTS
237
238
    /**
239
     * Process a variable assignment.
240
     *
241
     * @return  null
242
     */
243 8
    protected function variable_assignment() {
244 8
        $m = $this->current_match(); 
245 8
        $this->fetch_next_token();
246 8
        $def = $this->variable();
247 8
        $this->add_variable($m[1], $def);
248 8
    }
249
250
    /**
251
     * Process a rule declaration.
252
     *
253
     * @return  null
254
     */
255 11
    protected function rule_declaration() {
256 11
        if ($this->is_current_token_matched_by("only")) {
257 1
            $this->advance("only");
258 1
        }
259 11
        $var = $this->variable();
260 11
        $mode = $this->rule_mode();
261 11
        $schema = $this->rule_schema();
262 11
        $this->is_start_of_rule_arguments = true;
263 11
        $arguments = $schema->fetch_arguments($this);
264 11
        assert('is_array($arguments)');
265 11
        $this->rules[] = new R\Rule($mode, $var, $schema, $arguments);
266 11
    }
267
268
269
    // HANDLING OF VARIABLES
270
271
    /**
272
     * Add a variable to the variables currently known.
273
     *
274
     * @param   string      $name
275
     * @param   V\Variable  $def
276
     * @return null
277
     */
278 25
    protected function add_variable($name, V\Variable $def) {
279 25
        assert('is_string($name)');
280 25
        if (array_key_exists($name, $this->variables)) {
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
281
            throw new ParserException("Variable '$name' already defined.");
282
        }
283 25
        assert('$def instanceof Lechimp\\Dicto\\Variables\\Variable');
284 25
        $this->variables[$name] = $def->withName($name);
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
285 25
    }
286
287
    /**
288
     * Get a predefined variable.
289
     *
290
     * @param   string  $name
291
     * @return  V\Variable
292
     */
293 20
    protected function get_variable($name) {
294 20
        if (!array_key_exists($name, $this->variables)) {
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
295
            throw new ParserException("Unknown variable '$name'.");
296
        }
297 20
        return $this->variables[$name];
0 ignored issues
show
Bug introduced by
The property variables does not seem to exist. Did you mean predefined_variables?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
298
    }
299
300
    /**
301
     * Add all predefined variables to the current set of variables.
302
     *
303
     * @return null
304
     */
305 25
    protected function add_predefined_variables() {
306 25
        foreach ($this->predefined_variables as $predefined_var) {
307 25
            $this->add_variable($predefined_var->name(), $predefined_var);
308 25
        }
309 25
    }
310
311
    /**
312
     * Purge all predefined variables from the current set of variables.
313
     *
314
     * @return null
315
     */
316 16
    protected function purge_predefined_variables() {
317 16
        foreach ($this->predefined_variables as $predefined_var) {
318 16
            unset($this->variables[$predefined_var->name()]);
319 16
        }
320 16
    }
321
322
    // HANDLING OF SCHEMAS
323
324
    /**
325
     * Add a list of schemas to the parser.
326
     *
327
     * @param   Schema[]
328
     * @return  null
329
     */
330 25
    protected function add_schemas(array &$schemas) {
331 25
        foreach ($schemas as $schema) {
332 25
            $this->add_schema($schema);
333 25
        }
334 25
    }
335
336
    /**
337
     * Add a schema to the parser.
338
     *
339
     * @param   R/Schema
340
     * @return  null
341
     */
342 25
    protected function add_schema(R\Schema $schema) {
343 25
        $this->symbol($schema->name())
344 25
            ->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...
345 11
                return $schema;
346 25
            });
347 25
    }
348
349
    // IMPLEMENTATION OF ArgumentParser
350
351
    /**
352
     * @var bool
353
     */
354
    protected $is_start_of_rule_arguments = false;
355
356 11
    protected function maybe_fetch_argument_delimiter() {
357 11
        if (!$this->is_start_of_rule_arguments) {
358
            $this->advance_operator(",");
359
            $this->is_start_of_rule_arguments = false;
360
        }
361 11
    }
362
363
    /**
364
     * @inheritdoc
365
     */
366 5
    public function fetch_string() {
367 5
        $this->maybe_fetch_argument_delimiter();
368 5
        return $this->string();
369
    }
370
371
    /**
372
     * @inheritdoc
373
     */
374 6
    public function fetch_variable() {
375 6
        $this->maybe_fetch_argument_delimiter();
376 6
        return $this->variable();
377
    }
378
}
379