Completed
Push — master ( b93a87...00392a )
by Richard
05:47
created

RuleParser::add_symbols_for_comments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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