Test Failed
Push — master ( 3b4c09...efb6fb )
by Richard
02:50
created

ASTParser::string()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 0
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
/**
14
 * Parser for Rulesets.
15
 *
16
 * The grammar looks like this:
17
 *
18
 * explanation = '/ ** ... * /'
19
 * comment = '//..' | '/ * ... * /'
20
 * assignment = name "=" def
21
 * string = '"..."'
22
 * atom = [a-z ]...
23
 * name = [A-Z] [a-z]...
24
 * def = name | "{" def,... "}" | def property | def "except" def
25
 * property = atom ((name|string|atom)...)?
26
 * statement = def qualifier rule
27
 * rule = atom ((name|string|atom)...)?
28
 */
29
class ASTParser extends Parser {
30
    const EXPLANATION_RE = "[/][*][*](([^*]|([*][^/]))*)[*][/]";
31
    const SINGLE_LINE_COMMENT_RE = "[/][/]([^\n]*)";
32
    const MULTI_LINE_COMMENT_RE = "[/][*](([^*]|([*][^/]))*)[*][/]";
33
    const RULE_MODE_RE = "must|can(not)?";
34
    const STRING_RE = "[\"]((([\\\\][\"])|[^\"])+)[\"]";
35
    const NAME_RE = "[A-Z][A-Za-z_]*";
36
    const ATOM_RE = "[a-z ]*";
37
    const ASSIGNMENT_RE = "(".self::NAME_RE.")\s*=\s*";
38
    const ATOM_HEAD_RE = "(".self::ATOM_RE.")\s*:\s*";
39
40
    /**
41
     * @var AST\Factory
42
     */
43
    protected $ast_factory;
44
45
    public function __construct(AST\Factory $ast_factory) {
46
        $this->ast_factory = $ast_factory;
47
        parent::__construct();
48
    }
49
50
    // Definition of symbols in the parser
51
52
    /**
53
     * @inheritdocs
54
     */
55
    protected function add_symbols_to(SymbolTable $table) {
56
        $this->add_symbols_for_comments($table);
57
58
        $this->add_symbols_for_variables_to($table);
59
60
        $this->add_symbols_for_rules_to($table);
61
62
        // Assignment 
63
        $table->symbol(self::ASSIGNMENT_RE);
64
65
        // Strings
66
        $table->symbol(self::STRING_RE);
67
68
        // Names
69
        $table->literal(self::NAME_RE, function (array &$matches) {
70
            return $this->ast_factory->name($matches[0]);
71
        });
72
73
        // Head of a property or rule
74
        $table->symbol(self::ATOM_HEAD_RE, 20)
75
            ->left_denotation_is(function ($left, &$matches) {
76
                if (!($left instanceof AST\Definition)) {
77
                    throw new ParserException
78
                        ("Expected a variable at the left of \"{$matches[0]}\".");
79
                }
80
                $id = $this->ast_factory->atom($matches[1]);
81
                $arguments = $this->arguments();
82
                return $this->ast_factory->property($left, $id, $arguments);
83
            })
84
            ->null_denotation_is(function (&$matches) {
85
                return $this->ast_factory->atom($matches[1]);
86
            });
87
88
89
90
        // End of statement
91
        $table->symbol("\n");
92
    }
93
94
    /**
95
     * @param   SymbolTable
96
     * @return  null
97
     */
98
    protected function add_symbols_for_comments(SymbolTable $table) {
99
        $table->symbol(self::EXPLANATION_RE);
100
        $table->symbol(self::SINGLE_LINE_COMMENT_RE);
101
        $table->symbol(self::MULTI_LINE_COMMENT_RE);
102
    }
103
104
    /**
105
     * @param   SymbolTable
106
     * @return  null
107
     */
108
    protected function add_symbols_for_variables_to(SymbolTable $table) {
109
        // Any
110
        $table->operator("{")
111
            ->null_denotation_is(function() {
112
                $arr = array();
113
                while(true) {
114
                    $arr[] = $this->variable(0);
115
                    if ($this->is_current_token_operator("}")) {
116
                        $this->advance_operator("}");
117
                        return $this->ast_factory->any($arr);
118
                    }
119
                    $this->advance_operator(",");
120
                }
121
            });
122
        $table->operator("}");
123
        $table->operator(",");
124
125
        // Except
126
        $table->symbol("except", 10)
127
            ->left_denotation_is(function($left) {
128
                if (!($left instanceof AST\Definition)) {
129
                    throw new ParserException
130
                        ("Expected a variable at the left of except.");
131
                }
132
                $right = $this->variable(10);
133
                return $this->ast_factory->except($left, $right);
134
            });
135
    }
136
137
    /**
138
     * @param   SymbolTable
139
     * @return  null
140
     */
141
    protected function add_symbols_for_rules_to(SymbolTable $table) {
142
        // Rules
143
        $table->symbol("only");
144
        $table->symbol(self::RULE_MODE_RE, 0)
145
            ->null_denotation_is(function (array &$matches) {
146
                if ($matches[0] == "can") {
147
                    return $this->ast_factory->only_X_can();
148
                }
149
                if ($matches[0] == "must") {
150
                    return $this->ast_factory->must();
151
                }
152
                if ($matches[0] == "cannot") {
153
                    return $this->ast_factory->cannot();
154
                }
155
                throw new \LogicException("Unexpected \"".$matches[0]."\".");
156
            });
157
    }
158
159
    // IMPLEMENTATION OF Parser
160
161
    /**
162
     * @return  Ruleset
163
     */
164
    public function parse($source) {
165
        $this->variables = array();
0 ignored issues
show
Bug introduced by
The property variables 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...
166
        $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...
167
        return parent::parse($source);
168
    }
169
170
    /**
171
     * Root expression for the parser is some whitespace or comment where a
172
     * top level statement is in the middle.
173
     *
174
     * @return  Ruleset 
175
     */
176
    protected function root() {
177
        $lines = [];
178
        while (true) {
179
            // drop empty lines
180
            while ($tok = $this->is_current_token_to_be_dropped()) {
181
                $this->advance($tok);
182
            }
183
            if ($this->is_end_of_file_reached()) {
184
                break;
185
            }
186
187
            $lines[] = $this->top_level_statement();
188
        }
189
        return $this->ast_factory->root($lines);
190
    }
191
192
    /**
193
     * Parses the top level statements in the rules file.
194
     *
195
     * @return  null
196
     */
197
    public function top_level_statement() {
198
        // A top level statements is either..
199
        // ..an explanation
200
        if ($this->is_current_token_matched_by(self::EXPLANATION_RE)) {
201
            $m = $this->current_match();
202
            $this->advance(self::EXPLANATION_RE);
203
            return $this->ast_factory->explanation($this->trim_explanation($m[1]));
204
        }
205
        // ..an assignment to a variable.
206
        elseif ($this->is_current_token_matched_by(self::ASSIGNMENT_RE)) {
207
            return $this->variable_assignment();
208
        }
209
        // ..or a rule declaration
210
        else {
211
            return $this->rule_declaration();
212
        }
213
    }
214
215
    /**
216
     * Returns currently matched whitespace or comment token if there is any.
217
     *
218
     * @return string|null
219
     */
220
    public function is_current_token_to_be_dropped() {
221
        if ($this->is_current_token_matched_by("\n")) {
222
            return "\n";
223
        }
224
        if ($this->is_current_token_matched_by(self::SINGLE_LINE_COMMENT_RE)) {
225
            return self::SINGLE_LINE_COMMENT_RE;
226
        }
227
        if ($this->is_current_token_matched_by(self::MULTI_LINE_COMMENT_RE)) {
228
            return self::MULTI_LINE_COMMENT_RE;
229
        }
230
        return null;
231
    }
232
233
    /**
234
     * @param   string
235
     * @return  string
236
     */
237
    protected function trim_explanation($content) {
238
        return trim(
239
            preg_replace("%\s*\n\s*([*]\s*)?%s", "\n", $content)
240
        );
241
    }
242
243
    /**
244
     * Fetch a rule mode from the stream.
245
     *
246
     * @return mixed
247
     */
248
    protected function rule_mode() {
249
        $this->is_current_token_matched_by(self::RULE_MODE_RE);
250
        $t = $this->current_symbol();
251
        $m = $this->current_match();
252
        $this->fetch_next_token();
253
        $mode = $t->null_denotation($m);
254
        return $mode;
255
    }
256
257
    /**
258
     * Fetch a string from the stream.
259
     *
260
     * @return  string
261
     */
262
    protected function string() {
263
        $m = $this->current_match();
264
        $this->fetch_next_token();
265
        return $this->ast_factory->string_value
266
            (str_replace("\\\"", "\"",
267
                str_replace("\\n", "\n",
268
                    $m[1])));
269
    }
270
271
    /**
272
     * Fetch a variable from the stream.
273
     *
274
     * @return  V\Variable
275
     */
276
    protected function variable($right_binding_power = 0) {
277
        $t = $this->current_symbol();
278
        $m = $this->current_match();
279
        $this->fetch_next_token();
280
        $left = $t->null_denotation($m);
281
282
        while ($right_binding_power < $this->token[0]->binding_power()) {
283
            $t = $this->current_symbol();
284
            $m = $this->current_match();
285
            $this->fetch_next_token();
286
            $left = $t->left_denotation($left, $m);
287
        }
288
289
        if (!($left instanceof AST\Definition)) {
290
            throw new ParserException("Expected variable.");
291
        }
292
293
        return $left;
294
    }
295
296
    /**
297
     * Fetch some arguments from the stream.
298
     *
299
     * @return  array   of atoms, variables or strings
300
     */
301
    protected function arguments() {
302
        $args = [];
303
        while (true) {
304
            // An argument is either
305
            // ..a string
306
            if ($this->is_current_token_matched_by(self::STRING_RE)) {
307
                $m = $this->current_match();
308
                $this->fetch_next_token();
309
                $args[] = $this->ast_factory->string_value
310
                    (str_replace("\\\"", "\"",
311
                        str_replace("\\n", "\n",
312
                            $m[1])));
313
            }
314
            // ..a variable
315
            // TODO: this won't do with {..}
316
            if ($this->is_current_token_matched_by(self::NAME_RE)) {
317
                $args[] = $this->variable(0);
318
            }
319
            else {
320
                break;
321
            }
322
        }
323
        return $args; 
324
    }
325
326
    /**
327
     * Fetch a rule schema and its arguments from the stream.
328
     *
329
     * @return  array   (R\Schema, array)
330
     */
331
    protected function schema() {
332
        $t = $this->current_symbol();
333
        $m = $this->current_match();
334
        $this->fetch_next_token();
335
        $id = $t->null_denotation($m);
336
        if (!($id instanceof AST\Atom)) {
337
            throw new ParserException("Expected name of a rule schema.");
338
        }
339
        return $id;
340
    }
341
342
    // TOP LEVEL STATEMENTS
343
344
    /**
345
     * Process a variable assignment.
346
     *
347
     * @return  null
348
     */
349
    protected function variable_assignment() {
350
        $m = $this->current_match(); 
351
        $name = $this->ast_factory->name($m[1]);
352
        $this->fetch_next_token();
353
        $def = $this->variable();
354
        return $this->ast_factory->assignment($name, $def);
355
    }
356
357
    /**
358
     * Process a rule declaration.
359
     *
360
     * @return  null
361
     */
362
    protected function rule_declaration() {
363
        if ($this->is_current_token_matched_by("only")) {
364
            $this->advance("only");
365
        }
366
        $var = $this->variable();
367
        $mode = $this->rule_mode();
368
        $schema = $this->schema();
369
        $arguments = $this->arguments();
370
        assert('is_array($arguments)');
371
        return $this->ast_factory->rule
372
            ( $mode
373
            , $this->ast_factory->property
374
                ( $var
375
                , $schema
376
                , $arguments
377
                )
378
            );
379
    }
380
}
381