Completed
Push — master ( bd87f9...9971b9 )
by Richard
06:03
created

RuleParser::add_symbols_for_variables_to()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 4.0016

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 30
ccs 20
cts 21
cp 0.9524
rs 8.5806
cc 4
eloc 20
nc 1
nop 1
crap 4.0016
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 ASSIGNMENT_RE = "(\w+)\s*=\s*";
22
    const STRING_RE = "[\"]((([\\\\][\"])|[^\"])+)[\"]";
23
    const RULE_MODE_RE = "must|can(not)?";
24
25
    /**
26
     * @var V\Variable[]
27
     */
28
    protected $predefined_variables;
29
30
    /**
31
     * @var R\Schema[]
32
     */
33
    protected $known_schemas;
34
35
    /**
36
     * @var R\Property[]
37
     */
38
    protected $known_properties;
39
40
    /**
41
     * @var V\Variable[]
42
     */
43
    protected $variables = array();
44
45
    /**
46
     * @var R\Rule[]
47
     */
48
    protected $rules = array();
49
50 25
    public function __construct() {
51 25
        $this->predefined_variables = array
52 25
            ( new V\Classes()
53 25
            , new V\Functions()
54 25
            , new V\Globals()
55 25
            , new V\Files()
56 25
            , new V\Methods()
57
            // TODO: Add some language constructs here...
58 25
            );
59 25
        $this->known_schemas = array
60 25
            ( new R\ContainText()
61 25
            , new R\DependOn()
62 25
            , new R\Invoke()
63 25
            );
64 25
        $this->known_properties = array
0 ignored issues
show
Documentation Bug introduced by
It seems like array(new \Lechimp\Dicto\Variables\Name()) of type array<integer,object<Lec...to\\Variables\\Name>"}> is incompatible with the declared type array<integer,object<Lec...\Dicto\Rules\Property>> of property $known_properties.

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...
65 25
            ( new V\Name()
66 25
            );
67 25
        parent::__construct();
68 25
    }
69
70
    // Definition of symbols in the parser
71
72
    /**
73
     * @inheritdocs
74
     */
75 25
    protected function add_symbols_to(SymbolTable $table) {
76 25
        $this->add_symbols_for_variables_to($table);
77
78 25
        $this->add_symbols_for_rules_to($table);
79
80
        // Strings
81 25
        $table->symbol(self::STRING_RE);
82
83
        // Assignment 
84 25
        $table->symbol(self::ASSIGNMENT_RE);
85
86
        // Names
87
        $table->literal("\w+", function (array &$matches) {
88 20
                return $this->get_variable($matches[0]);
89 25
            });
90
91
        // End of statement
92 25
        $table->symbol("\n");
93 25
    }
94
95
    /**
96
     * @param   SymbolTable
97
     * @return  null
98
     */
99 25
    protected function add_symbols_for_variables_to(SymbolTable $table) {
100
        // Any
101 25
        $table->operator("{")
102
            ->null_denotation_is(function() {
103 7
                $arr = array();
104 7
                while(true) {
105 7
                    $arr[] = $this->variable(0);
106 7
                    if ($this->is_current_token_operator("}")) {
107 7
                        $this->advance_operator("}");
108 7
                        return new V\Any($arr);
109
                    }
110 7
                    $this->advance_operator(",");
111 7
                }
112 25
            });
113 25
        $table->operator("}");
114 25
        $table->operator(",");
115
116
        // Except
117 25
        $table->symbol("except", 10)
118
            ->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...
119 7
                if (!($left instanceof V\Variable)) {
120
                    throw new ParserException
121
                        ("Expected a variable at the left of except.");
122
                }
123 7
                $right = $this->variable(10);
124 7
                return new V\Except($left, $right);
125 25
            });
126
127 25
        $this->add_symbols_for_properties_to($table, $this->known_properties);
128 25
    }
129
130
    /**
131
     * @param   SymbolTable     $table
132
     * @param   V\Property[]    $properties
133
     * @return  null
134
     */
135 25
    protected function add_symbols_for_properties_to(SymbolTable $table, array &$properties) {
136 25
        foreach ($properties as $property) {
137 25
            $table->symbol($property->parse_as().":", 20)
138
                ->left_denotation_is(function($left) use ($property) {
139 7
                    if (!($left instanceof V\Variable)) {
140
                        throw new ParserException
141
                            ("Expected a variable at the left of \"with name:\".");
142
                    }
143 7
                    $this->is_start_of_rule_arguments = true;
144 7
                    $arguments = $property->fetch_arguments($this);
145 7
                    assert('is_array($arguments)');
146 7
                    return new V\WithProperty($left, $property, $arguments);
147 25
                });
148 25
        }
149 25
    }
150
151
    /**
152
     * @param   SymbolTable
153
     * @return  null
154
     */
155 25
    protected function add_symbols_for_rules_to(SymbolTable $table) {
156
        // Rules
157 25
        $table->symbol("only");
158 25
        $table->symbol(self::RULE_MODE_RE, 0)
159
            ->null_denotation_is(function (array &$matches) {
160 11
                if ($matches[0] == "can") {
161 1
                    return R\Rule::MODE_ONLY_CAN;
162
                }
163 10
                if ($matches[0] == "must") {
164 7
                    return R\Rule::MODE_MUST;
165
                }
166 3
                if ($matches[0] == "cannot") {
167 3
                    return R\Rule::MODE_CANNOT;
168
                }
169
                throw new \LogicException("Unexpected \"".$matches[0]."\".");
170 25
            });
171 25
        $this->add_symbols_for_schemas_to($table, $this->known_schemas);
172 25
    }
173
174
    /**
175
     * @param   SymbolTable     $table
176
     * @param   R\Schema[]    $schemas
177
     * @return  null
178
     */
179 25
    protected function add_symbols_for_schemas_to(SymbolTable $table, array &$schemas) {
180 25
        foreach ($schemas as $schema) {
181 25
            $table->symbol($schema->name())
182 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...
183 11
                    return $schema;
184 25
                });
185 25
        }
186 25
    }
187
188
    // IMPLEMENTATION OF Parser
189
190
    /**
191
     * @return  Ruleset
192
     */
193 25
    public function parse($source) {
194 25
        $this->variables = array();
195 25
        $this->rules = array();
196 25
        $this->add_predefined_variables();
197 25
        return parent::parse($source);
198
    }
199
200
    /**
201
     * Parses the top level statements in the rules file.
202
     *
203
     * @return  Ruleset 
204
     */
205 16
    protected function root() {
206 16
        while (true) {
207
            // drop empty lines
208 16
            while ($this->is_current_token_matched_by("\n")) {
209 5
                $this->advance("\n");
210 5
            }
211 16
            if ($this->is_end_of_file_reached()) {
212 6
                break;
213
            }
214
215
            // A top level statments is either..
216
            // ..an assignment to a variable.
217 15
            if ($this->is_current_token_matched_by(self::ASSIGNMENT_RE)) {
218 8
                $this->variable_assignment();
219 8
            }
220
            // ..or a rule declaration
221
            else {
222 11
                $this->rule_declaration();
223
            }
224
225 15
            if ($this->is_end_of_file_reached()) {
226 10
                break;
227
            }
228 6
            $this->advance("\n");
229 6
        }
230 16
        $this->purge_predefined_variables();
231 16
        return new Ruleset($this->variables, $this->rules);
232
    }
233
234
    // EXPRESSION TYPES
235
236
    /**
237
     * Fetch a rule mode from the stream.
238
     *
239
     * @return mixed
240
     */
241 11
    protected function rule_mode() {
242 11
        $this->is_current_token_matched_by(self::RULE_MODE_RE);
243 11
        $t = $this->current_symbol();
244 11
        $m = $this->current_match();
245 11
        $this->fetch_next_token();
246 11
        $mode = $t->null_denotation($m);
247 11
        return $mode;
248
    }
249
250
    /**
251
     * Fetch a string from the stream.
252
     *
253
     * @return  string
254
     */
255 15
    protected function string() {
256 15
        if (!$this->is_current_token_matched_by(self::STRING_RE)) {
257
            throw new ParserException("Expected string.");
258
        }
259 15
        $m = $this->current_match();
260 15
        $this->fetch_next_token();
261 15
        return  str_replace("\\\"", "\"",
262 15
                    str_replace("\\n", "\n",
263 15
                        $m[1]));
264
    }
265
266
    /**
267
     * Fetch a variable from the stream.
268
     *
269
     * @return  V\Variable
270
     */
271 20
    protected function variable($right_binding_power = 0) {
272 20
        $t = $this->current_symbol();
273 20
        $m = $this->current_match();
274 20
        $this->fetch_next_token();
275 20
        $left = $t->null_denotation($m);
276
277 20
        while ($right_binding_power < $this->token[0]->binding_power()) {
278 10
            $t = $this->current_symbol();
279 10
            $m = $this->current_match();
280 10
            $this->fetch_next_token();
281 10
            $left = $t->left_denotation($left, $m);
282 10
        }
283
284 20
        if (!($left instanceof V\Variable)) {
285
            throw new ParserException("Expected variable.");
286
        }
287
288 20
        return $left;
289
    }
290
291
    /**
292
     * Fetch a rule schema and its arguments from the stream.
293
     *
294
     * @return  array   (R\Schema, array)
295
     */
296 11
    protected function rule_schema() {
297 11
        $t = $this->current_symbol();
298 11
        $m = $this->current_match();
299 11
        $this->fetch_next_token();
300 11
        $schema = $t->null_denotation($m);
301 11
        if (!($schema instanceof R\Schema)) {
302
            throw new ParserException("Expected name of a rule schema.");
303
        }
304 11
        return $schema;
305
    }
306
307
    // TOP LEVEL STATEMENTS
308
309
    /**
310
     * Process a variable assignment.
311
     *
312
     * @return  null
313
     */
314 8
    protected function variable_assignment() {
315 8
        $m = $this->current_match(); 
316 8
        $this->fetch_next_token();
317 8
        $def = $this->variable();
318 8
        $this->add_variable($m[1], $def);
319 8
    }
320
321
    /**
322
     * Process a rule declaration.
323
     *
324
     * @return  null
325
     */
326 11
    protected function rule_declaration() {
327 11
        if ($this->is_current_token_matched_by("only")) {
328 1
            $this->advance("only");
329 1
        }
330 11
        $var = $this->variable();
331 11
        $mode = $this->rule_mode();
332 11
        $schema = $this->rule_schema();
333 11
        $this->is_start_of_rule_arguments = true;
334 11
        $arguments = $schema->fetch_arguments($this);
335 11
        assert('is_array($arguments)');
336 11
        $this->rules[] = new R\Rule($mode, $var, $schema, $arguments);
337 11
    }
338
339
340
    // HANDLING OF VARIABLES
341
342
    /**
343
     * Add a variable to the variables currently known.
344
     *
345
     * @param   string      $name
346
     * @param   V\Variable  $def
347
     * @return null
348
     */
349 25
    protected function add_variable($name, V\Variable $def) {
350 25
        assert('is_string($name)');
351 25
        if (array_key_exists($name, $this->variables)) {
352
            throw new ParserException("Variable '$name' already defined.");
353
        }
354 25
        assert('$def instanceof Lechimp\\Dicto\\Variables\\Variable');
355 25
        $this->variables[$name] = $def->withName($name);
356 25
    }
357
358
    /**
359
     * Get a predefined variable.
360
     *
361
     * @param   string  $name
362
     * @return  V\Variable
363
     */
364 20
    protected function get_variable($name) {
365 20
        if (!array_key_exists($name, $this->variables)) {
366
            throw new ParserException("Unknown variable '$name'.");
367
        }
368 20
        return $this->variables[$name];
369
    }
370
371
    /**
372
     * Add all predefined variables to the current set of variables.
373
     *
374
     * @return null
375
     */
376 25
    protected function add_predefined_variables() {
377 25
        foreach ($this->predefined_variables as $predefined_var) {
378 25
            $this->add_variable($predefined_var->name(), $predefined_var);
379 25
        }
380 25
    }
381
382
    /**
383
     * Purge all predefined variables from the current set of variables.
384
     *
385
     * @return null
386
     */
387 16
    protected function purge_predefined_variables() {
388 16
        foreach ($this->predefined_variables as $predefined_var) {
389 16
            unset($this->variables[$predefined_var->name()]);
390 16
        }
391 16
    }
392
393
    // IMPLEMENTATION OF ArgumentParser
394
395
    /**
396
     * @var bool
397
     */
398
    protected $is_start_of_rule_arguments = false;
399
400 13
    protected function maybe_fetch_argument_delimiter() {
401 13
        if (!$this->is_start_of_rule_arguments) {
402
            $this->advance_operator(",");
403
            $this->is_start_of_rule_arguments = false;
404
        }
405 13
    }
406
407
    /**
408
     * @inheritdoc
409
     */
410 11
    public function fetch_string() {
411 11
        $this->maybe_fetch_argument_delimiter();
412 11
        return $this->string();
413
    }
414
415
    /**
416
     * @inheritdoc
417
     */
418 6
    public function fetch_variable() {
419 6
        $this->maybe_fetch_argument_delimiter();
420 6
        return $this->variable();
421
    }
422
}
423