Completed
Push — master ( f7852e...e666ea )
by Richard
06:08
created

RuleParser::rule_declaration()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 3

Importance

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