Passed
Push — master ( 766df6...286284 )
by Roman
55s
created

Parser   C

Complexity

Total Complexity 49

Size/Duplication

Total Lines 332
Duplicated Lines 6.02 %

Coupling/Cohesion

Components 2
Dependencies 18

Importance

Changes 0
Metric Value
dl 20
loc 332
rs 5.1442
c 0
b 0
f 0
wmc 49
lcom 2
cbo 18

19 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 6 1
B parseToken() 0 19 6
D parseExpression() 0 28 10
A parseDeclaration() 0 10 2
A parseIfExpression() 0 10 2
A parseOrExpression() 10 10 2
A parseAndExpression() 10 10 2
A parseLambdaDeclaration() 0 16 2
A parseFunctionDeclaration() 0 22 3
A parseAssignDeclaration() 0 17 3
A parseCallExpression() 0 9 1
A parseProgram() 0 4 1
A parseSequence() 0 4 1
A parseIdentifier() 0 4 1
A parseDotIdentifier() 0 4 1
A parseString() 0 4 1
A parseNumeric() 0 4 1
B deflate() 0 22 4
B findPairClosingBracketIndex() 0 20 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PeacefulBit\Slate\Parser;
4
5
use function Nerd\Common\Arrays\all;
6
use function Nerd\Common\Arrays\toHeadTail;
7
use function Nerd\Common\Arrays\append;
8
9
use function Nerd\Common\Functional\tail;
10
11
use PeacefulBit\Slate\Exceptions\ParserException;
12
use PeacefulBit\Slate\Parser\Nodes;
13
use PeacefulBit\Slate\Parser\Tokens;
14
15
class Parser
16
{
17
    /**
18
     * Convert tokens tree to abstract syntax tree.
19
     *
20
     * @param Tokens\Token[] $tokens
21
     * @return Nodes\Node
22
     */
23
    public function parse(array $tokens): Nodes\Node
24
    {
25
        $tokensTree = $this->deflate($tokens);
26
27
        return $this->parseProgram($tokensTree);
28
    }
29
30
    /**
31
     * @param $token
32
     * @return Nodes\Identifier|Nodes\Literal|Nodes\Node
33
     * @throws ParserException
34
     */
35
    private function parseToken($token)
36
    {
37
        if (is_array($token)) {
38
            return $this->parseExpression($token);
39
        }
40
        if ($token instanceof Tokens\StringToken) {
41
            return $this->parseString($token);
42
        }
43
        if ($token instanceof Tokens\IdentifierToken) {
44
            return $this->parseIdentifier($token);
45
        }
46
        if ($token instanceof Tokens\DotIdentifierToken) {
47
            return $this->parseDotIdentifier($token);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->parseDotIdentifier($token); (PeacefulBit\Slate\Parser\Nodes\DotIdentifier) is incompatible with the return type documented by PeacefulBit\Slate\Parser\Parser::parseToken of type PeacefulBit\Slate\Parser\Nodes\Node.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
48
        }
49
        if ($token instanceof Tokens\NumericToken) {
50
            return $this->parseNumeric($token);
51
        }
52
        throw new ParserException("Unexpected type of token - $token.");
53
    }
54
55
    /**
56
     * @param array $tokens
57
     * @return Nodes\Node
58
     * @throws ParserException
59
     */
60
    private function parseExpression(array $tokens): Nodes\Node
61
    {
62
        if (empty($tokens)) {
63
            throw new ParserException("Expression could not be empty.");
64
        }
65
66
        list ($head, $tail) = toHeadTail($tokens);
67
68
        if ($head instanceof Tokens\IdentifierToken) {
69
            switch ($head->getValue()) {
70
                case 'def':
71
                    return $this->parseDeclaration($tail);
72
                case 'lambda':
73
                    return $this->parseLambdaDeclaration($tail);
74
                case 'if':
75
                    return $this->parseIfExpression($tail);
76
                case 'or':
77
                    return $this->parseOrExpression($tail);
78
                case 'and':
79
                    return $this->parseAndExpression($tail);
80
                case 'use':
81
                case 'cond':
82
                    throw new ParserException('This keyword is reserved');
83
            }
84
        }
85
86
        return $this->parseCallExpression($tokens);
87
    }
88
89
    /**
90
     * @param array $tokens
91
     * @return Nodes\Node
92
     */
93
    private function parseDeclaration($tokens): Nodes\Node
94
    {
95
        list ($head, $tail) = $tokens;
0 ignored issues
show
Unused Code introduced by
The assignment to $tail is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
96
97
        if (!is_array($head)) {
98
            return $this->parseAssignDeclaration($tokens);
99
        }
100
101
        return $this->parseFunctionDeclaration($tokens);
102
    }
103
104
    /**
105
     * @param $tokens
106
     * @return Nodes\Node
107
     * @throws ParserException
108
     */
109
    private function parseIfExpression($tokens): Nodes\Node
110
    {
111
        if (sizeof($tokens) != 3) {
112
            throw new ParserException("'if' requires exactly three arguments");
113
        }
114
115
        list ($test, $cons, $alt) = array_map([$this, 'parseToken'], $tokens);
116
117
        return new Nodes\IfExpression($test, $cons, $alt);
118
    }
119
120
    /**
121
     * @param $tokens
122
     * @return Nodes\Node
123
     * @throws ParserException
124
     */
125 View Code Duplication
    private function parseOrExpression($tokens): Nodes\Node
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126
    {
127
        if (empty($tokens)) {
128
            throw new ParserException("'or' requires at least one argument");
129
        }
130
131
        $expressions = array_map([$this, 'parseToken'], $tokens);
132
133
        return new Nodes\OrExpression($expressions);
134
    }
135
136
    /**
137
     * @param $tokens
138
     * @return Nodes\Node
139
     * @throws ParserException
140
     */
141 View Code Duplication
    private function parseAndExpression($tokens): Nodes\Node
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
142
    {
143
        if (empty($tokens)) {
144
            throw new ParserException("'and' requires at least one argument");
145
        }
146
147
        $expressions = array_map([$this, 'parseToken'], $tokens);
148
149
        return new Nodes\AndExpression($expressions);
150
    }
151
152
    /**
153
     * @param $tokens
154
     * @return Nodes\Node
155
     * @throws ParserException
156
     */
157
    private function parseLambdaDeclaration($tokens): Nodes\Node
158
    {
159
        list ($head, $body) = toHeadTail($tokens);
160
161
        $test = all($head, function ($token) {
162
            return $token instanceof Tokens\IdentifierToken;
163
        });
164
165
        if (!$test) {
166
            throw new ParserException("Lambda arguments must be valid identifiers.");
167
        }
168
169
        $params = array_map([$this, 'parseToken'], $head);
170
171
        return new Nodes\LambdaExpression($params, $this->parseSequence($body));
172
    }
173
174
    /**
175
     * @param $tokens
176
     * @return Nodes\Node
177
     * @throws ParserException
178
     */
179
    private function parseFunctionDeclaration($tokens): Nodes\Node
180
    {
181
        list ($head, $body) = toHeadTail($tokens);
182
183
        if (sizeof($head) < 1) {
184
            throw new ParserException("Function must have identifier.");
185
        }
186
187
        $test = all($head, function ($token) {
188
            return $token instanceof Tokens\IdentifierToken;
189
        });
190
191
        if (!$test) {
192
            throw new ParserException("Function name and arguments must be valid identifiers.");
193
        }
194
195
        list ($name, $args) = toHeadTail($head);
196
197
        $params = array_map([$this, 'parseToken'], $args);
198
199
        return new Nodes\FunctionExpression($name->getValue(), $params, $this->parseSequence($body));
0 ignored issues
show
Bug introduced by
The method getValue cannot be called on $name (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
200
    }
201
202
    /**
203
     * @param array $tokens
204
     * @return Nodes\Assign
205
     * @throws ParserException
206
     */
207
    private function parseAssignDeclaration(array $tokens): Nodes\Assign
208
    {
209
        if (sizeof($tokens) % 2 != 0) {
210
            throw new ParserException("Bad assign declaration");
211
        }
212
213
        $assignments = array_chunk($tokens, 2);
214
215
        $result = array_map(function ($assign) {
216
            if (!$assign[0] instanceof Tokens\IdentifierToken) {
217
                throw new ParserException("Bad type of identifier in assign declaration");
218
            }
219
            return array_map([$this, 'parseToken'], $assign);
220
        }, $assignments);
221
222
        return new Nodes\Assign($result);
223
    }
224
225
    /**
226
     * @param array $tokens
227
     * @return Nodes\Node
228
     */
229
    private function parseCallExpression(array $tokens): Nodes\Node
230
    {
231
        list ($head, $tail) = toHeadTail($tokens);
232
233
        return new Nodes\CallExpression(
234
            $this->parseToken($head),
235
            array_map([$this, 'parseToken'], $tail)
236
        );
237
    }
238
239
    /**
240
     * @param array $tokens
241
     * @return Nodes\Program
242
     */
243
    private function parseProgram(array $tokens): Nodes\Program
244
    {
245
        return new Nodes\Program(array_map([$this, 'parseToken'], $tokens));
246
    }
247
248
    /**
249
     * @param array $tokens
250
     * @return Nodes\SequenceExpression
251
     */
252
    private function parseSequence(array $tokens): Nodes\SequenceExpression
253
    {
254
        return new Nodes\SequenceExpression(array_map([$this, 'parseToken'], $tokens));
255
    }
256
257
    /**
258
     * @param Tokens\IdentifierToken $token
259
     * @return Nodes\Identifier
260
     */
261
    private function parseIdentifier(Tokens\IdentifierToken $token): Nodes\Identifier
262
    {
263
        return new Nodes\Identifier($token->getValue());
264
    }
265
266
    /**
267
     * @param Tokens\DotIdentifierToken $token
268
     * @return Nodes\DotIdentifier
269
     */
270
    private function parseDotIdentifier(Tokens\DotIdentifierToken $token): Nodes\DotIdentifier
271
    {
272
        return new Nodes\DotIdentifier($token->getValue());
273
    }
274
275
    /**
276
     * @param Tokens\StringToken $token
277
     * @return Nodes\Literal
278
     */
279
    private function parseString(Tokens\StringToken $token): Nodes\Literal
280
    {
281
        return new Nodes\StringNode($token->getValue());
282
    }
283
284
    /**
285
     * @param Tokens\NumericToken $token
286
     * @return Nodes\Literal
287
     */
288
    private function parseNumeric(Tokens\NumericToken $token): Nodes\Literal
289
    {
290
        return new Nodes\Number($token->getValue());
291
    }
292
293
    /**
294
     * Convert list of tokens into token tree.
295
     *
296
     * @param Tokens\Token[] $tokens
297
     * @return mixed
298
     */
299
    private function deflate(array $tokens)
300
    {
301
        $iter = tail(function ($rest, $acc) use (&$iter) {
302
            if (empty($rest)) {
303
                return $acc;
304
            }
305
            list ($head, $tail) = toHeadTail($rest);
306
            switch (get_class($head)) {
307
                case Tokens\OpenBracketToken::class:
308
                    $pairClosingIndex = $this->findPairClosingBracketIndex($tail);
309
                    $inner = array_slice($tail, 0, $pairClosingIndex);
310
                    $innerNode = $this->deflate($inner);
311
                    $newTail = array_slice($tail, $pairClosingIndex + 1);
312
                    return $iter($newTail, append($acc, $innerNode));
313
                case Tokens\CloseBracketToken::class:
314
                    throw new ParserException("Unpaired opening bracket found");
315
                default:
316
                    return $iter($tail, append($acc, $head));
317
            }
318
        });
319
        return $iter($tokens, []);
320
    }
321
322
    /**
323
     * @param array $tokens
324
     * @return mixed
325
     */
326
    private function findPairClosingBracketIndex(array $tokens)
327
    {
328
        $iter = tail(function ($rest, $depth, $position) use (&$iter) {
329
            if (empty($rest)) {
330
                throw new ParserException("Unpaired closing bracket found.");
331
            }
332
            list ($head, $tail) = toHeadTail($rest);
333
            if ($head instanceof Tokens\CloseBracketToken) {
334
                if ($depth == 0) {
335
                    return $position;
336
                }
337
                return $iter($tail, $depth - 1, $position + 1);
338
            }
339
            if ($head instanceof Tokens\OpenBracketToken) {
340
                return $iter($tail, $depth + 1, $position + 1);
341
            }
342
            return $iter($tail, $depth, $position + 1);
343
        });
344
        return $iter($tokens, 0, 0);
345
    }
346
}
347